Resumen de las elecciones a la Comunidad de Madrid 2021

0.-Objetivo del presente trabajo

El presente trabajo tiene como objetivo analizar los resultados de las elecciones autonómicas del 4 de mayo del 2021 en Madrid y sacar conclusiones sobre la distribución del voto mediante representaciones espaciales, así como su comparación con pasadas elecciones para sacar conclusiones de cómo ha ido variando.

Nota sobre la obtención de los datos:

Los datos de las últimas elecciones se obtuvieron del siguiente enlace: https://resultados2021.comunidad.madrid/Mesas/es. Los pasos para analizar la información y procesarla están resumidos en el siguiente documento: Madrid_Elections_2021.ipynb . Al no conseguir datos similares con el mismo formato de pasadas elecciones autonómicas se optó por hacer un “web scrapper” de la página del periódico El País donde se resumen los votos obtenidos en las últimas 5 elecciones (2021, 2019, 2015, 2011 y 2007).

1.-Obtención y preparación de los datos

Obtenemos los datos de las elecciones autonómicas de Madrid 2019 y 2021 haciendo “scrapping” de las siguientes páginas:

Para poder procesar la información de la web hemos tenido que instalar los siguientes paquetes:

  • Instalamos beautifulsoup4: pip install beautifulsoup4

  • Intalamos lxml: pip install lxml

  • Instalamos requests library: pip install requests

Analizando la web a la que nos lleva la primera url vemos el resumen de enlaces que a su vez llevan a los datos de cada municipio. Estos enlaces están alojados en elementos ‘ul’ que a su vez tienen una lista de elementos ‘li’ con enlaces que llevan a cada página.

Vamos a obtener la lista resumen de esos elementos ‘ul’, ‘li’ y los enlaces para después procesar cada página:

from bs4 import BeautifulSoup
import requests

1.0.-Preparación de los datos de 2019:

Obtenemos el texto html de la web y vemos que el elemento ul donde se aloja el resumen de municipios tiene la clase ‘estirar’:

html_text = requests.get('https://resultados.elpais.com/elecciones/2019/autonomicas/12/').text
soup = BeautifulSoup(html_text, 'lxml')
# soup
ul = soup.select('ul.estirar')[1]
# ul

El resumen de elementos ‘li’:

lis = ul.find_all('li') 
# lis

Vamos a crear un array resumen que contenga el nombre de cada municipio y su link asociado:

url = 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/'
results_2019 = []
for li in lis:
    for link in li.find_all('a'):
        local_result = {}
        local_result['municipio'] = link.text
        local_result['link'] = url+link.get('href')
        results_2019.append(local_result)

results_2019[0]
{'municipio': 'Ajalvir',
 'link': 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/28/02.html'}

1.0.0-Extracción de los datos de cada municipio

Vamos a hacer una prueba con una de las páginas donde se resumen los datos de un municipio para ver cómo tenemos que extraer los datos y después aplicarlo al resumen de municipios y links que hemos obtenido en results_2019.

Hacemos la prueba con el municipio de Madarcos:

# link de pruebas (municipio Madarcos):
link_pruebas = 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/28/78.html'

# obtenemos el código html:
html_text_municipio = requests.get(link_pruebas).text
soup = BeautifulSoup(html_text_municipio, 'lxml')
# soup

Después de analizar dónde están los datos que buscamos podemos diferenciar dos tablas resumen con los siguientes identificadores:

  • Tabla de escrutado: ‘id’=’tablaResumen’

  • Tabla resumen partidos: ‘id’=’tablaVotosPartidos’

Tabla de escrutado:

Vemos que obtenemos los siguientes campos:

table_escrutado = soup.find('table', {'id': 'tablaResumen'})
table_escrutado_trs = table_escrutado.find_all('tr')
table_escrutado_trs
[<tr>
 <th class="encabezado">Escrutado:</th>
 <td class="tipoPorciento" colspan="2">100 %</td>
 </tr>,
 <tr>
 <th class="encabezado">Votos contabilizados:</th>
 <td class="tipoNumero">37</td>
 <td class="tipoPorciento">92,5 %</td>
 </tr>,
 <tr>
 <th class="encabezado">Abstenciones:</th>
 <td class="tipoNumero">3</td>
 <td class="tipoPorciento">7,5 %</td>
 </tr>,
 <tr>
 <th class="encabezado">Votos nulos:</th>
 <td class="tipoNumero">0</td>
 <td class="tipoPorciento">0 %</td>
 </tr>,
 <tr>
 <th class="encabezado">Votos en blanco:</th>
 <td class="tipoNumero">0</td>
 <td class="tipoPorciento">0 %</td>
 </tr>]

Y que podemos resumir los datos de la siguiente manera:

results_table_escrutado = []
for tr in table_escrutado_trs:
    local_table_escrutado = {}
    local_table_escrutado['encabezado'] = None if tr.select_one('.encabezado') is None else tr.select_one('.encabezado').text
    local_table_escrutado['numero'] = None if tr.select_one('.tipoNumero') is None else tr.select_one('.tipoNumero').text
    local_table_escrutado['porcentaje'] = None if tr.select_one('.tipoPorciento') is None else tr.select_one('.tipoPorciento').text
    
    results_table_escrutado.append(local_table_escrutado)
    
results_table_escrutado
[{'encabezado': 'Escrutado:', 'numero': None, 'porcentaje': '100 %'},
 {'encabezado': 'Votos contabilizados:',
  'numero': '37',
  'porcentaje': '92,5 %'},
 {'encabezado': 'Abstenciones:', 'numero': '3', 'porcentaje': '7,5 %'},
 {'encabezado': 'Votos nulos:', 'numero': '0', 'porcentaje': '0 %'},
 {'encabezado': 'Votos en blanco:', 'numero': '0', 'porcentaje': '0 %'}]

De aquí podemos aplicar unas funciones para extraer los datos de los campos comunes en cada diccionario (encabezado, número y porcentaje) y pasar los valores de string a numéricos:

def clean_strings_and_turn_float(value):
    if ' %' in value:
        return value.replace(' %', '').replace(',', '.')
    else:
        return value.replace('.', '')
    
def result_resume(results_table_escrutado):
    results_resume = []
    for result in results_table_escrutado:
        local_resume = {}
        if result.get('encabezado') == 'Escrutado:':
            local_resume['escrutado'] = clean_strings_and_turn_float(result.get('porcentaje'))
        if result.get('encabezado') == 'Votos contabilizados:':
            local_resume['votos_totales'] = clean_strings_and_turn_float(result.get('numero'))
            local_resume['votos_totales_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
        if result.get('encabezado') == 'Abstenciones:':
            local_resume['abstencion'] = clean_strings_and_turn_float(result.get('numero'))
            local_resume['abstencion_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
        if result.get('encabezado') == 'Votos nulos:':
            local_resume['votos_nulos'] = clean_strings_and_turn_float(result.get('numero'))
            local_resume['votos_nulos_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
        if result.get('encabezado') == 'Votos en blanco:':
            local_resume['votos_blancos'] = clean_strings_and_turn_float(result.get('numero'))
            local_resume['votos_blancos_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
        
        results_resume.append(local_resume)

    return results_resume

result_resume(results_table_escrutado)
[{'escrutado': '100'},
 {'votos_totales': '37', 'votos_totales_porcentaje': '92.5'},
 {'abstencion': '3', 'abstencion_porcentaje': '7.5'},
 {'votos_nulos': '0', 'votos_nulos_porcentaje': '0'},
 {'votos_blancos': '0', 'votos_blancos_porcentaje': '0'}]

Tabla resumen partidos:

Vemos que obtenemos los siguientes campos:

table_partidos = soup.find('table', {'id': 'tablaVotosPartidos'})
table_partido_trs = table_partidos.find_all('tr')
table_partido_trs
[<tr>
 <th class="encabezado">Partido</th>
 <th class="encabezado">Votos</th>
 <th class="encabezado">%</th>
 </tr>,
 <tr><th class="nombrePartido"><acronym title="PARTIDO POPULAR">PP</acronym></th><td class="tipoNumeroVotos">15</td><td class="tipoPorcientoVotos">40,54 %</td></tr>,
 <tr><th class="nombrePartido"><acronym title="UNIDAS PODEMOS">PODEMOS-IU</acronym></th><td class="tipoNumeroVotos">7</td><td class="tipoPorcientoVotos">18,92 %</td></tr>,
 <tr><th class="nombrePartido"><acronym title="PARTIDO SOCIALISTA OBRERO ESPAÑOL">PSOE</acronym></th><td class="tipoNumeroVotos">6</td><td class="tipoPorcientoVotos">16,22 %</td></tr>,
 <tr><th class="nombrePartido"><acronym title="CIUDADANOS-PARTIDO DE LA CIUDADANIA">Cs</acronym></th><td class="tipoNumeroVotos">3</td><td class="tipoPorcientoVotos">8,11 %</td></tr>,
 <tr><th class="nombrePartido"><acronym title="MÁS MADRID">MÁS MADRID</acronym></th><td class="tipoNumeroVotos">3</td><td class="tipoPorcientoVotos">8,11 %</td></tr>,
 <tr><th class="nombrePartido"><acronym title="VOX">VOX</acronym></th><td class="tipoNumeroVotos">2</td><td class="tipoPorcientoVotos">5,41 %</td></tr>,
 <tr><th class="nombrePartido"><acronym title="PARTIDO ANIMALISTA CONTRA EL MALTRATO ANIMAL">PACMA</acronym></th><td class="tipoNumeroVotos">1</td><td class="tipoPorcientoVotos">2,7 %</td></tr>]
results_table_partido = []
for tr in table_partido_trs:
    local_table_partido = {}
    local_table_partido['partido'] = None if tr.select_one('.nombrePartido') is None else tr.select_one('.nombrePartido').text
    local_table_partido['numero_votos'] = None if tr.select_one('.tipoNumeroVotos') is None else tr.select_one('.tipoNumeroVotos').text
    local_table_partido['porcentaje'] = None if tr.select_one('.tipoPorcientoVotos') is None else tr.select_one('.tipoPorcientoVotos').text
    
    results_table_partido.append(local_table_partido)
    
results_table_partido
[{'partido': None, 'numero_votos': None, 'porcentaje': None},
 {'partido': 'PP', 'numero_votos': '15', 'porcentaje': '40,54 %'},
 {'partido': 'PODEMOS-IU', 'numero_votos': '7', 'porcentaje': '18,92 %'},
 {'partido': 'PSOE', 'numero_votos': '6', 'porcentaje': '16,22 %'},
 {'partido': 'Cs', 'numero_votos': '3', 'porcentaje': '8,11 %'},
 {'partido': 'MÁS MADRID', 'numero_votos': '3', 'porcentaje': '8,11 %'},
 {'partido': 'VOX', 'numero_votos': '2', 'porcentaje': '5,41 %'},
 {'partido': 'PACMA', 'numero_votos': '1', 'porcentaje': '2,7 %'}]

Vamos a hacer un tratamiento parecido al caso de la tabla de escrutado: vamos a reducir los nombres de cada partido a minúsculas, si tienen más de una palabra las unimos por guiones y eliminamos acentos; los valores los pasamos de strings a valores numéricos y eliminamos los signos de porcentajes.

import unicodedata

def strip_accents(text):
    try:
        text = unicode(text, 'utf-8')
    except NameError: # unicode is a default on python 3 
        pass

    text = unicodedata.normalize('NFD', text)\
           .encode('ascii', 'ignore')\
           .decode("utf-8")

    return str(text)
    
def result_partido_resume(results_table_partido):
    results_resume_partido = []
    for result in results_table_partido:
        local_resume = {}
        if result.get('partido') == None:
            continue
        else:
            partido = strip_accents(result.get('partido').lower().replace('-', '_').replace(' ', '_'))
            local_resume[partido] = clean_strings_and_turn_float(result.get('numero_votos'))
            local_resume[partido+'_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
    
        results_resume_partido.append(local_resume)

    return results_resume_partido

result_partido_resume(results_table_partido)
[{'pp': '15', 'pp_porcentaje': '40.54'},
 {'podemos_iu': '7', 'podemos_iu_porcentaje': '18.92'},
 {'psoe': '6', 'psoe_porcentaje': '16.22'},
 {'cs': '3', 'cs_porcentaje': '8.11'},
 {'mas_madrid': '3', 'mas_madrid_porcentaje': '8.11'},
 {'vox': '2', 'vox_porcentaje': '5.41'},
 {'pacma': '1', 'pacma_porcentaje': '2.7'}]

Ya podemos aplicar sobre todos los municipios. Para ellos definimos dos funciones que resumen lo que hemos hecho en ambas tablas:

def table_escrutado(link):
    html_text_municipio = requests.get(link).text
    soup = BeautifulSoup(html_text_municipio, 'lxml')

    table_escrutado = soup.find('table', {'id': 'tablaResumen'})
    table_escrutado_trs = table_escrutado.find_all('tr')
    
    results_table_escrutado = []
    for tr in table_escrutado_trs:
        local_table_escrutado = {}
        local_table_escrutado['encabezado'] = None if tr.select_one('.encabezado') is None else tr.select_one('.encabezado').text
        local_table_escrutado['numero'] = None if tr.select_one('.tipoNumero') is None else tr.select_one('.tipoNumero').text
        local_table_escrutado['porcentaje'] = None if tr.select_one('.tipoPorciento') is None else tr.select_one('.tipoPorciento').text
    
        results_table_escrutado.append(local_table_escrutado)
    
    return results_table_escrutado
def table_partido(link):
    html_text_municipio = requests.get(link).text
    soup = BeautifulSoup(html_text_municipio, 'lxml')
    
    table_partido = soup.find('table', {'id': 'tablaVotosPartidos'})
    table_partido_trs = table_partido.find_all('tr')
    
    results_table_partido = []
    for tr in table_partido_trs:
        local_table_partido = {}
        local_table_partido['partido'] = None if tr.select_one('.nombrePartido') is None else tr.select_one('.nombrePartido').text
        local_table_partido['numero_votos'] = None if tr.select_one('.tipoNumeroVotos') is None else tr.select_one('.tipoNumeroVotos').text
        local_table_partido['porcentaje'] = None if tr.select_one('.tipoPorcientoVotos') is None else tr.select_one('.tipoPorcientoVotos').text
    
        results_table_partido.append(local_table_partido)
        
    return results_table_partido

Aplicamos sobre la url inicial desde la que accederemos a todos los municipios:

url = 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/'
results_pruebas = []
for li in lis:
    for link in li.find_all('a'):
        local_result = {}
        local_result['municipio'] = link.text
        local_result['link'] = url+link.get('href')
        local_result['escrutinio'] = table_escrutado(local_result['link'])
        local_result['partidos'] = table_partido(local_result['link'])
        results_pruebas.append(local_result)
        
results_pruebas[0]
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-15-3d2d3fa4870b> in <module>
      6         local_result['municipio'] = link.text
      7         local_result['link'] = url+link.get('href')
----> 8         local_result['escrutinio'] = table_escrutado(local_result['link'])
      9         local_result['partidos'] = table_partido(local_result['link'])
     10         results_pruebas.append(local_result)

<ipython-input-13-9e9d090d3b01> in table_escrutado(link)
      1 def table_escrutado(link):
----> 2     html_text_municipio = requests.get(link).text
      3     soup = BeautifulSoup(html_text_municipio, 'lxml')
      4 
      5     table_escrutado = soup.find('table', {'id': 'tablaResumen'})

~/anaconda3/lib/python3.8/site-packages/requests/api.py in get(url, params, **kwargs)
     74 
     75     kwargs.setdefault('allow_redirects', True)
---> 76     return request('get', url, params=params, **kwargs)
     77 
     78 

~/anaconda3/lib/python3.8/site-packages/requests/api.py in request(method, url, **kwargs)
     59     # cases, and look like a memory leak in others.
     60     with sessions.Session() as session:
---> 61         return session.request(method=method, url=url, **kwargs)
     62 
     63 

~/anaconda3/lib/python3.8/site-packages/requests/sessions.py in request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    540         }
    541         send_kwargs.update(settings)
--> 542         resp = self.send(prep, **send_kwargs)
    543 
    544         return resp

~/anaconda3/lib/python3.8/site-packages/requests/sessions.py in send(self, request, **kwargs)
    653 
    654         # Send the request
--> 655         r = adapter.send(request, **kwargs)
    656 
    657         # Total elapsed time of the request (approximately)

~/anaconda3/lib/python3.8/site-packages/requests/adapters.py in send(self, request, stream, timeout, verify, cert, proxies)
    437         try:
    438             if not chunked:
--> 439                 resp = conn.urlopen(
    440                     method=request.method,
    441                     url=url,

~/anaconda3/lib/python3.8/site-packages/urllib3/connectionpool.py in urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    697 
    698             # Make the request on the httplib connection object.
--> 699             httplib_response = self._make_request(
    700                 conn,
    701                 method,

~/anaconda3/lib/python3.8/site-packages/urllib3/connectionpool.py in _make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    443                     # Python 3 (including for exceptions like SystemExit).
    444                     # Otherwise it looks like a bug in the code.
--> 445                     six.raise_from(e, None)
    446         except (SocketTimeout, BaseSSLError, SocketError) as e:
    447             self._raise_timeout(err=e, url=url, timeout_value=read_timeout)

~/anaconda3/lib/python3.8/site-packages/urllib3/packages/six.py in raise_from(value, from_value)

~/anaconda3/lib/python3.8/site-packages/urllib3/connectionpool.py in _make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    438                 # Python 3
    439                 try:
--> 440                     httplib_response = conn.getresponse()
    441                 except BaseException as e:
    442                     # Remove the TypeError from the exception chain in

~/anaconda3/lib/python3.8/http/client.py in getresponse(self)
   1345         try:
   1346             try:
-> 1347                 response.begin()
   1348             except ConnectionError:
   1349                 self.close()

~/anaconda3/lib/python3.8/http/client.py in begin(self)
    305         # read until we get a non-100 response
    306         while True:
--> 307             version, status, reason = self._read_status()
    308             if status != CONTINUE:
    309                 break

~/anaconda3/lib/python3.8/http/client.py in _read_status(self)
    266 
    267     def _read_status(self):
--> 268         line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
    269         if len(line) > _MAXLINE:
    270             raise LineTooLong("status line")

~/anaconda3/lib/python3.8/socket.py in readinto(self, b)
    667         while True:
    668             try:
--> 669                 return self._sock.recv_into(b)
    670             except timeout:
    671                 self._timeout_occurred = True

~/anaconda3/lib/python3.8/ssl.py in recv_into(self, buffer, nbytes, flags)
   1239                   "non-zero flags not allowed in calls to recv_into() on %s" %
   1240                   self.__class__)
-> 1241             return self.read(nbytes, buffer)
   1242         else:
   1243             return super().recv_into(buffer, nbytes, flags)

~/anaconda3/lib/python3.8/ssl.py in read(self, len, buffer)
   1097         try:
   1098             if buffer is not None:
-> 1099                 return self._sslobj.read(len, buffer)
   1100             else:
   1101                 return self._sslobj.read(len)

KeyboardInterrupt: 

Vamos a chequear qué longitud tiene nuestra lista:

len(results_pruebas)
178

Vemos que tiene una longitud de 178 cuando se sabe que la Comunidad de Madrid tiene 179 municipios. Por lo tanto la página de inicio tiene un fallo y no presenta la información de un municipio.

Sabemos que el municipio que falta es La Acebeda. Más adelante cuando pasemos los resultados a un dataframe y le añadamos la información geográfica explicaremos cómo hemos sabido que era ese municipio.

Buscando de forma manual llegamos que la información electoral de La Acebeda para el año 2019 está resumida en este enlace: https://resultados.elpais.com/elecciones/2019/autonomicas/12/28/01.html

# insertamos la info. asociada a La Acebeda:
la_acebeda = {}
la_acebeda['municipio'] = 'La Acebeda'
la_acebeda['link'] = 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/28/01.html'
la_acebeda['escrutinio'] = table_escrutado(la_acebeda['link'])
la_acebeda['partidos'] = table_partido(la_acebeda['link'])
results_pruebas.insert(0, la_acebeda)

results_pruebas[0]
{'municipio': 'La Acebeda',
 'link': 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/28/01.html',
 'escrutinio': [{'encabezado': 'Escrutado:',
   'numero': None,
   'porcentaje': '100 %'},
  {'encabezado': 'Votos contabilizados:',
   'numero': '70',
   'porcentaje': '90,91 %'},
  {'encabezado': 'Abstenciones:', 'numero': '7', 'porcentaje': '9,09 %'},
  {'encabezado': 'Votos nulos:', 'numero': '3', 'porcentaje': '4,29 %'},
  {'encabezado': 'Votos en blanco:', 'numero': '0', 'porcentaje': '0 %'}],
 'partidos': [{'partido': None, 'numero_votos': None, 'porcentaje': None},
  {'partido': 'PP', 'numero_votos': '25', 'porcentaje': '37,31 %'},
  {'partido': 'Cs', 'numero_votos': '14', 'porcentaje': '20,9 %'},
  {'partido': 'PSOE', 'numero_votos': '11', 'porcentaje': '16,42 %'},
  {'partido': 'MÁS MADRID', 'numero_votos': '10', 'porcentaje': '14,93 %'},
  {'partido': 'PODEMOS-IU', 'numero_votos': '5', 'porcentaje': '7,46 %'},
  {'partido': 'VOX', 'numero_votos': '2', 'porcentaje': '2,99 %'}]}

Pasamos a presentar la información de una forma que nos sea más fácil de tratar como dataframe:

results_pruebas_formatted = []

for result_prueba in results_pruebas:
    local_result = {}
    local_result['municipio'] = result_prueba['municipio']
    local_result['link'] = result_prueba['link']
    local_result['escrutinio'] = result_resume(result_prueba['escrutinio'])
    local_result['partidos'] = result_partido_resume(result_prueba['partidos'])
    
    results_pruebas_formatted.append(local_result)
    
results_pruebas_formatted[0]
{'municipio': 'La Acebeda',
 'link': 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/28/01.html',
 'escrutinio': [{'escrutado': '100'},
  {'votos_totales': '70', 'votos_totales_porcentaje': '90.91'},
  {'abstencion': '7', 'abstencion_porcentaje': '9.09'},
  {'votos_nulos': '3', 'votos_nulos_porcentaje': '4.29'},
  {'votos_blancos': '0', 'votos_blancos_porcentaje': '0'}],
 'partidos': [{'pp': '25', 'pp_porcentaje': '37.31'},
  {'cs': '14', 'cs_porcentaje': '20.9'},
  {'psoe': '11', 'psoe_porcentaje': '16.42'},
  {'mas_madrid': '10', 'mas_madrid_porcentaje': '14.93'},
  {'podemos_iu': '5', 'podemos_iu_porcentaje': '7.46'},
  {'vox': '2', 'vox_porcentaje': '2.99'}]}

1.0.1-Convertir la información en dataframe

Vamos a crear un dataframe donde se muestre la información de cada municipio y el resumen de partidos a los que se ha votado en cada uno.

import pandas as pd

df_partidos = pd.json_normalize(results_pruebas_formatted, record_path='partidos', meta=['municipio', 'link'])
df_partidos = df_partidos.groupby('municipio').apply(lambda x: x.bfill().head(1)).reset_index(drop=True)

df_escrutinio = pd.json_normalize(results_pruebas_formatted, record_path='escrutinio', meta=['municipio'])
df_escrutinio = df_escrutinio.groupby('municipio').apply(lambda x: x.bfill().head(1)).reset_index(drop=True)

df = pd.merge(df_partidos, df_escrutinio, on='municipio', how='outer')
df
pp pp_porcentaje cs cs_porcentaje psoe psoe_porcentaje mas_madrid mas_madrid_porcentaje podemos_iu podemos_iu_porcentaje ... link escrutado votos_totales votos_totales_porcentaje abstencion abstencion_porcentaje votos_nulos votos_nulos_porcentaje votos_blancos votos_blancos_porcentaje
0 521 24.05 456 21.05 532 24.56 224 10.34 81 3.74 ... https://resultados.elpais.com/elecciones/2019/... 100 2178 66.34 1105 33.66 12 0.55 27 1.25
1 41 29.29 19 13.57 54 38.57 12 8.57 3 2.14 ... https://resultados.elpais.com/elecciones/2019/... 100 140 82.84 29 17.16 0 0 0 0
2 15953 17.91 18120 20.34 28759 32.28 10975 12.32 5306 5.96 ... https://resultados.elpais.com/elecciones/2019/... 100 89514 65.71 46719 34.29 434 0.48 424 0.48
3 13844 25.38 11032 20.22 15338 28.12 6047 11.08 2527 4.63 ... https://resultados.elpais.com/elecciones/2019/... 100 54711 67.79 25992 32.21 157 0.29 189 0.35
4 16844 19.29 16214 18.57 27492 31.48 12227 14 6537 7.49 ... https://resultados.elpais.com/elecciones/2019/... 100 87720 69.08 39267 30.92 387 0.44 424 0.49
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
174 249 26.38 175 18.54 229 24.26 105 11.12 53 5.61 ... https://resultados.elpais.com/elecciones/2019/... 100 946 65.33 502 34.67 2 0.21 2 0.21
175 778 20.24 839 21.83 1488 38.71 261 6.79 147 3.82 ... https://resultados.elpais.com/elecciones/2019/... 100 3882 73.2 1421 26.8 38 0.98 31 0.81
176 4628 30.76 3416 22.7 2762 18.36 1508 10.02 523 3.48 ... https://resultados.elpais.com/elecciones/2019/... 100 15105 72.93 5606 27.07 59 0.39 60 0.4
177 40 24.54 23 14.11 47 28.83 18 11.04 16 9.82 ... https://resultados.elpais.com/elecciones/2019/... 100 163 82.32 35 17.68 0 0 0 0
178 193 23.89 77 9.53 244 30.2 102 12.62 109 13.49 ... https://resultados.elpais.com/elecciones/2019/... 100 821 72.27 315 27.73 13 1.58 3 0.37

179 rows × 41 columns

# cambiamos la posición de las columnas para una mejor presentación:
first_column = df.pop('municipio')
second_column = df.pop('link')
df.insert(0, 'municipio', first_column)
df.insert(1, 'link', second_column)
df
municipio link pp pp_porcentaje cs cs_porcentaje psoe psoe_porcentaje mas_madrid mas_madrid_porcentaje ... uleg_porcentaje escrutado votos_totales votos_totales_porcentaje abstencion abstencion_porcentaje votos_nulos votos_nulos_porcentaje votos_blancos votos_blancos_porcentaje
0 Ajalvir https://resultados.elpais.com/elecciones/2019/... 521 24.05 456 21.05 532 24.56 224 10.34 ... NaN 100 2178 66.34 1105 33.66 12 0.55 27 1.25
1 Alameda del Valle https://resultados.elpais.com/elecciones/2019/... 41 29.29 19 13.57 54 38.57 12 8.57 ... NaN 100 140 82.84 29 17.16 0 0 0 0
2 Alcalá de Henares https://resultados.elpais.com/elecciones/2019/... 15953 17.91 18120 20.34 28759 32.28 10975 12.32 ... 0.01 100 89514 65.71 46719 34.29 434 0.48 424 0.48
3 Alcobendas https://resultados.elpais.com/elecciones/2019/... 13844 25.38 11032 20.22 15338 28.12 6047 11.08 ... 0.03 100 54711 67.79 25992 32.21 157 0.29 189 0.35
4 Alcorcón https://resultados.elpais.com/elecciones/2019/... 16844 19.29 16214 18.57 27492 31.48 12227 14 ... 0.02 100 87720 69.08 39267 30.92 387 0.44 424 0.49
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
174 Villar del Olmo https://resultados.elpais.com/elecciones/2019/... 249 26.38 175 18.54 229 24.26 105 11.12 ... NaN 100 946 65.33 502 34.67 2 0.21 2 0.21
175 Villarejo de Salvanés https://resultados.elpais.com/elecciones/2019/... 778 20.24 839 21.83 1488 38.71 261 6.79 ... NaN 100 3882 73.2 1421 26.8 38 0.98 31 0.81
176 Villaviciosa de Odón https://resultados.elpais.com/elecciones/2019/... 4628 30.76 3416 22.7 2762 18.36 1508 10.02 ... 0.02 100 15105 72.93 5606 27.07 59 0.39 60 0.4
177 Villavieja del Lozoya https://resultados.elpais.com/elecciones/2019/... 40 24.54 23 14.11 47 28.83 18 11.04 ... NaN 100 163 82.32 35 17.68 0 0 0 0
178 Zarzalejo https://resultados.elpais.com/elecciones/2019/... 193 23.89 77 9.53 244 30.2 102 12.62 ... NaN 100 821 72.27 315 27.73 13 1.58 3 0.37

179 rows × 41 columns

df.count()
municipio                    179
link                         179
pp                           179
pp_porcentaje                179
cs                           179
cs_porcentaje                179
psoe                         179
psoe_porcentaje              179
mas_madrid                   178
mas_madrid_porcentaje        178
podemos_iu                   179
podemos_iu_porcentaje        179
vox                          179
vox_porcentaje               179
pacma                        159
pacma_porcentaje             159
fe_de_las_jons               134
fe_de_las_jons_porcentaje    134
pum+j                        120
pum+j_porcentaje             120
pcte                         113
pcte_porcentaje              113
upyd                         120
upyd_porcentaje              120
ph                            96
ph_porcentaje                 96
pcas_tc                       95
pcas_tc_porcentaje            95
p_lib                         90
p_lib_porcentaje              90
uleg                          71
uleg_porcentaje               71
escrutado                    179
votos_totales                179
votos_totales_porcentaje     179
abstencion                   179
abstencion_porcentaje        179
votos_nulos                  179
votos_nulos_porcentaje       179
votos_blancos                179
votos_blancos_porcentaje     179
dtype: int64

1.0.2-Añadir información geoespacial al dataframe:

La información geoespacial la hemos obtenido del siguiente enlace: https://raw.githubusercontent.com/FMullor/TopoJson/master/MadridMunicipios.geojson.

Nos va a servir para poder realizar mapas de la distribución del voto en cada municipio.

import geopandas as gpd
import matplotlib.pyplot as plt

# sacamos la info del link y ordenamos por orden alfabético los municipios:
municipios = 'https://raw.githubusercontent.com/FMullor/TopoJson/master/MadridMunicipios.geojson'
map_municipios = gpd.read_file(municipios)
map_municipios = map_municipios.sort_values('municipio')
map_municipios.head()
id_0 iso pais id_1 communidad_ id_2 provincia id_3 name_3 id_4 ... varname_4 ccn_4 cca_4 type_4 engtype_4 cpro cmun dc codigo_post geometry
156 215.0 ESP Spain 8.0 Comunidad de Madrid 33.0 Madrid 234.0 n.a. (176) 5876.0 ... None 0.0 None Municipality Municipality 28 002 9 28002 MULTIPOLYGON (((-3.51150 40.53889, -3.50521 40...
58 215.0 ESP Spain 8.0 Comunidad de Madrid 33.0 Madrid 233.0 n.a. (175) 5828.0 ... None 0.0 None Municipality Municipality 28 003 5 28003 MULTIPOLYGON (((-3.80596 40.89047, -3.80951 40...
86 215.0 ESP Spain 8.0 Comunidad de Madrid 33.0 Madrid 234.0 n.a. (176) 5877.0 ... None 0.0 None Municipality Municipality 28 005 3 28005 MULTIPOLYGON (((-3.32142 40.47207, -3.32898 40...
168 215.0 ESP Spain 8.0 Comunidad de Madrid 33.0 Madrid 235.0 n.a. (177) 5907.0 ... None 0.0 None Municipality Municipality 28 006 6 28006 MULTIPOLYGON (((-3.67417 40.58897, -3.65981 40...
154 215.0 ESP Spain 8.0 Comunidad de Madrid 33.0 Madrid 235.0 n.a. (177) 5908.0 ... None 0.0 None Municipality Municipality 28 007 2 28007 MULTIPOLYGON (((-3.78781 40.35875, -3.79893 40...

5 rows × 21 columns

map_municipios.info()
<class 'geopandas.geodataframe.GeoDataFrame'>
Int64Index: 182 entries, 156 to 70
Data columns (total 21 columns):
 #   Column       Non-Null Count  Dtype   
---  ------       --------------  -----   
 0   id_0         182 non-null    float64 
 1   iso          182 non-null    object  
 2   pais         182 non-null    object  
 3   id_1         182 non-null    float64 
 4   communidad_  182 non-null    object  
 5   id_2         182 non-null    float64 
 6   provincia    182 non-null    object  
 7   id_3         182 non-null    float64 
 8   name_3       182 non-null    object  
 9   id_4         182 non-null    float64 
 10  municipio    182 non-null    object  
 11  varname_4    2 non-null      object  
 12  ccn_4        182 non-null    float64 
 13  cca_4        0 non-null      object  
 14  type_4       182 non-null    object  
 15  engtype_4    182 non-null    object  
 16  cpro         164 non-null    object  
 17  cmun         164 non-null    object  
 18  dc           164 non-null    object  
 19  codigo_post  164 non-null    object  
 20  geometry     182 non-null    geometry
dtypes: float64(6), geometry(1), object(14)
memory usage: 31.3+ KB

Como podemos ver, el geodataframe map_municipios tiene como máximo 182 registros. A la hora de poder cruzar la información con nuestros dataframes, vemos que la columna ‘municipio’ tiene 182 registros, lo que significa que hay 3 elementos de más sabiendo que la Comunidad de Madrid tiene 179 municipios. Tenemos que investigar si hay elementos repetitivos y cuáles son. Una vez limpio, solo nos interesan las columnas ‘municipio’ y ‘geometry’:

map_municipios = map_municipios[['municipio', 'geometry']]
map_municipios
municipio geometry
156 Ajalvir MULTIPOLYGON (((-3.51150 40.53889, -3.50521 40...
58 Alameda del Valle MULTIPOLYGON (((-3.80596 40.89047, -3.80951 40...
86 Alcalá de Henares MULTIPOLYGON (((-3.32142 40.47207, -3.32898 40...
168 Alcobendas MULTIPOLYGON (((-3.67417 40.58897, -3.65981 40...
154 Alcorcón MULTIPOLYGON (((-3.78781 40.35875, -3.79893 40...
... ... ...
166 Villar del Olmo MULTIPOLYGON (((-3.21619 40.31261, -3.22639 40...
175 Villarejo de Salvanés MULTIPOLYGON (((-3.20168 40.08538, -3.20231 40...
173 Villaviciosa de Odón MULTIPOLYGON (((-4.00598 40.33916, -3.99856 40...
178 Villavieja del Lozoya MULTIPOLYGON (((-3.65536 41.00760, -3.65717 41...
70 Zarzalejo MULTIPOLYGON (((-4.15354 40.53144, -4.16337 40...

182 rows × 2 columns

Vemos que hay municipios cuyo nombre al tener acentos o caracteres especiales no aparece correctamente y no va a coincidir con la información de nuestro dataframe. Para obtenerlos, hacemos:

no_intersection_map = set(map_municipios['municipio']).difference(set(df['municipio']))
len(no_intersection_map)
no_intersection_map
{'Alcalá de Henares',
 'Alcorcón',
 'Carabaña',
 'ChapinerÃ\xada',
 'Chinchón',
 'Cobeña',
 'El Vellón',
 'El Ã\x81lamo',
 'Fuentidueña de Tajo',
 'Griñón',
 'Horcajo de la Sierra',
 'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)',
 'Jurisdicción Mancomunada de Cerdedilla y Navacerrada',
 'Leganés',
 'Morata de Tajuña',
 'Móstoles',
 'Navarredonda y San Mamés',
 'Nuevo Baztán',
 'Orusco de Tajuña',
 'Perales de Tajuña',
 'Pinuécar-Gandullas',
 'Pozuelo de Alarcón',
 'Prádena del Rincón',
 'RascafrÃ\xada',
 'Redueña',
 'San AgustÃ\xadn del Guadalix',
 'San MartÃ\xadn de Valdeiglesias',
 'San MartÃ\xadn de la Vega',
 'San Sebastián de los Reyes',
 'Santa MarÃ\xada de la Alameda',
 'Torrejón de Ardoz',
 'Torrejón de Velasco',
 'Torrejón de la Calzada',
 'Valdepiélagos',
 'Valverde de Alcalá',
 'Villanueva de la Cañada',
 'Villarejo de Salvanés',
 'Villaviciosa de Odón'}

Por lo tanto, procedemos a corregir los nombres:

map_municipios['municipio'] = map_municipios['municipio'].replace([
 'Alcalá de Henares',
 'Alcorcón',
 'Carabaña',
 'ChapinerÃ\xada',
 'Chinchón',
 'Cobeña',
 'El Vellón',
 'El Ã\x81lamo',
 'Fuentidueña de Tajo',
 'Griñón',
 'Horcajo de la Sierra',
 'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)',
 'Jurisdicción Mancomunada de Cerdedilla y Navacerrada',
 'Leganés',
 'Morata de Tajuña',
 'Móstoles',
 'Navarredonda y San Mamés',
 'Nuevo Baztán',
 'Orusco de Tajuña',
 'Perales de Tajuña',
 'Pinuécar-Gandullas',
 'Pozuelo de Alarcón',
 'Prádena del Rincón',
 'RascafrÃ\xada',
 'Redueña',
 'San AgustÃ\xadn del Guadalix',
 'San MartÃ\xadn de Valdeiglesias',
 'San MartÃ\xadn de la Vega',
 'San Sebastián de los Reyes',
 'Santa MarÃ\xada de la Alameda',
 'Torrejón de Ardoz',
 'Torrejón de Velasco',
 'Torrejón de la Calzada',
 'Valdepiélagos',
 'Valverde de Alcalá',
 'Villanueva de la Cañada',
 'Villarejo de Salvanés',
 'Villaviciosa de Odón'
], [
 'Alcalá de Henares',
 'Alcorcón',
 'Carabaña',
 'Chapinería',
 'Chinchón',
 'Cobeña',
 'El Vellón',
 'El Álamo',
 'Fuentidueña de Tajo',
 'Griñón',
 'Horcajo de la Sierra',
 'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)',
 'Jurisdicción Mancomunada de Cerdedilla y Navacerrada',
 'Leganés',
 'Morata de Tajuña',
 'Móstoles',
 'Navarredonda y San Mamés',
 'Nuevo Baztán',
 'Orusco de Tajuña',
 'Perales de Tajuña',
 'Piñuécar-Gandullas',
 'Pozuelo de Alarcón',
 'Prádena del Rincón',
 'Rascafría',
 'Redueña',
 'San Agustín del Guadalix',
 'San Martín de Valdeiglesias',
 'San Martín de la Vega',
 'San Sebastián de los Reyes',
 'Santa María de la Alameda',
 'Torrejón de Ardoz',
 'Torrejón de Velasco',
 'Torrejón de la Calzada',
 'Valdepiélagos',
 'Valverde de Alcalá',
 'Villanueva de la Cañada',
 'Villarejo de Salvanés',
 'Villaviciosa de Odón'])

map_municipios.head()
municipio geometry
156 Ajalvir MULTIPOLYGON (((-3.51150 40.53889, -3.50521 40...
58 Alameda del Valle MULTIPOLYGON (((-3.80596 40.89047, -3.80951 40...
86 Alcalá de Henares MULTIPOLYGON (((-3.32142 40.47207, -3.32898 40...
168 Alcobendas MULTIPOLYGON (((-3.67417 40.58897, -3.65981 40...
154 Alcorcón MULTIPOLYGON (((-3.78781 40.35875, -3.79893 40...

Vemos si hay valores repetidos:

# sacamos los nombres de los municipios y los metemos en una lista:
municipios = map_municipios['municipio'].to_list()
#comprobamos qué nombres de municipios pueden estar repetidos:
set([x for x in municipios if municipios.count(x) > 1])
{'Arroyomolinos'}

Vemos si los valores de ‘geometry’ de ‘Arroyomolinos’ son los mismos y por lo tanto se pueden eliminar sin problema:

map_municipios[map_municipios['municipio'] == 'Arroyomolinos']
municipio geometry
60 Arroyomolinos MULTIPOLYGON (((-3.94179 40.29215, -3.93664 40...
89 Arroyomolinos MULTIPOLYGON (((-3.94179 40.29215, -3.93664 40...

Son los mismos, se puede eliminar un registro:

# nos quedamos solo con un valor de Arroyomolinos (keep = 'first'):
map_municipios.drop_duplicates(subset ="municipio", keep = 'first', inplace = True)
map_municipios[map_municipios['municipio'] == 'Arroyomolinos']
municipio geometry
60 Arroyomolinos MULTIPOLYGON (((-3.94179 40.29215, -3.93664 40...

Vemos si pueden haber diferencias entre los nombres de los municipios:

no_intersection_map = set(map_municipios['municipio']).difference(set(df['municipio']))
len(no_intersection_map)
no_intersection_map
{'Horcajo de la Sierra',
 'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)',
 'Jurisdicción Mancomunada de Cerdedilla y Navacerrada'}

Comprobamos con qué nombres se corresponderían estos municipios de map_municipios con los que tenemos en nuestro dataframe:

  • Horcajo de la Sierra : Horcajo de la Sierra-Aoslos,

  • Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral) : Manzanares el Real,

  • Jurisdicción Mancomunada de Cerdedilla y Navacerrada : Navacerrada

Antes de corregir los nombres, comprobamos si ya existen en map_municipios:

map_municipios[map_municipios['municipio'] == 'Horcajo de la Sierra-Aoslos']
municipio geometry
map_municipios[map_municipios['municipio'] == 'Manzanares el Real']
municipio geometry
12 Manzanares el Real MULTIPOLYGON (((-3.90600 40.68217, -3.90882 40...
map_municipios[map_municipios['municipio'] == 'Navacerrada']
municipio geometry
171 Navacerrada MULTIPOLYGON (((-3.96854 40.76714, -3.97736 40...

Se ve que ‘Horcajo de la Sierra-Aoslos’ es el único que no existe, por lo tanto se puede reemplazar.

Tenemos que ver si los otros dos pares de nombres de municipios tienen los mismos valores de ‘geometry’:

geometry_1 = map_municipios[map_municipios['municipio'] == 'Manzanares el Real']['geometry']
geometry_2 = map_municipios[map_municipios['municipio'] == 'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)']['geometry'] 

print(geometry_1)
print(geometry_2)
12    MULTIPOLYGON (((-3.90600 40.68217, -3.90882 40...
Name: geometry, dtype: geometry
74    MULTIPOLYGON (((-3.92431 40.67975, -3.91307 40...
Name: geometry, dtype: geometry
geometry_3 = map_municipios[map_municipios['municipio'] == 'Navacerrada']['geometry']
geometry_4 = map_municipios[map_municipios['municipio'] == 'Jurisdicción Mancomunada de Cerdedilla y Navacerrada']['geometry'] 

print(geometry_3)
print(geometry_4)
171    MULTIPOLYGON (((-3.96854 40.76714, -3.97736 40...
Name: geometry, dtype: geometry
109    MULTIPOLYGON (((-4.00244 40.78843, -3.99731 40...
Name: geometry, dtype: geometry

No tienen el mismo valor de ‘geometry’, por lo tanto solo vamos a corregir ‘Horcajo de la Sierra’ y los otros dos desaparecerán a la hora de mergear con nuestro dataframe:

map_municipios['municipio'] = map_municipios['municipio'].replace({
    'Horcajo de la Sierra': 'Horcajo de la Sierra-Aoslos',
})

no_intersection_map = set(map_municipios['municipio']).difference(set(df['municipio']))
len(no_intersection_map)
no_intersection_map
{'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)',
 'Jurisdicción Mancomunada de Cerdedilla y Navacerrada'}

Ya podemos mergear para que nuestro dataframe tenga información geoespacial:

df_2019 = pd.merge(df, map_municipios, how='left', on='municipio')
df_2019.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 179 entries, 0 to 178
Data columns (total 42 columns):
 #   Column                     Non-Null Count  Dtype   
---  ------                     --------------  -----   
 0   municipio                  179 non-null    object  
 1   link                       179 non-null    object  
 2   pp                         179 non-null    object  
 3   pp_porcentaje              179 non-null    object  
 4   cs                         179 non-null    object  
 5   cs_porcentaje              179 non-null    object  
 6   psoe                       179 non-null    object  
 7   psoe_porcentaje            179 non-null    object  
 8   mas_madrid                 178 non-null    object  
 9   mas_madrid_porcentaje      178 non-null    object  
 10  podemos_iu                 179 non-null    object  
 11  podemos_iu_porcentaje      179 non-null    object  
 12  vox                        179 non-null    object  
 13  vox_porcentaje             179 non-null    object  
 14  pacma                      159 non-null    object  
 15  pacma_porcentaje           159 non-null    object  
 16  fe_de_las_jons             134 non-null    object  
 17  fe_de_las_jons_porcentaje  134 non-null    object  
 18  pum+j                      120 non-null    object  
 19  pum+j_porcentaje           120 non-null    object  
 20  pcte                       113 non-null    object  
 21  pcte_porcentaje            113 non-null    object  
 22  upyd                       120 non-null    object  
 23  upyd_porcentaje            120 non-null    object  
 24  ph                         96 non-null     object  
 25  ph_porcentaje              96 non-null     object  
 26  pcas_tc                    95 non-null     object  
 27  pcas_tc_porcentaje         95 non-null     object  
 28  p_lib                      90 non-null     object  
 29  p_lib_porcentaje           90 non-null     object  
 30  uleg                       71 non-null     object  
 31  uleg_porcentaje            71 non-null     object  
 32  escrutado                  179 non-null    object  
 33  votos_totales              179 non-null    object  
 34  votos_totales_porcentaje   179 non-null    object  
 35  abstencion                 179 non-null    object  
 36  abstencion_porcentaje      179 non-null    object  
 37  votos_nulos                179 non-null    object  
 38  votos_nulos_porcentaje     179 non-null    object  
 39  votos_blancos              179 non-null    object  
 40  votos_blancos_porcentaje   179 non-null    object  
 41  geometry                   179 non-null    geometry
dtypes: geometry(1), object(41)
memory usage: 60.1+ KB

Pasamos las columnas a valores numéricos, excepto ‘municipio’, ‘link’ y ‘geometry’:

cols = df_2019.columns.drop(['municipio', 'link', 'geometry'])

df_2019[cols] = df_2019[cols].apply(pd.to_numeric, errors='coerce', axis=1)
df_2019.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 179 entries, 0 to 178
Data columns (total 42 columns):
 #   Column                     Non-Null Count  Dtype   
---  ------                     --------------  -----   
 0   municipio                  179 non-null    object  
 1   link                       179 non-null    object  
 2   pp                         179 non-null    float64 
 3   pp_porcentaje              179 non-null    float64 
 4   cs                         179 non-null    float64 
 5   cs_porcentaje              179 non-null    float64 
 6   psoe                       179 non-null    float64 
 7   psoe_porcentaje            179 non-null    float64 
 8   mas_madrid                 178 non-null    float64 
 9   mas_madrid_porcentaje      178 non-null    float64 
 10  podemos_iu                 179 non-null    float64 
 11  podemos_iu_porcentaje      179 non-null    float64 
 12  vox                        179 non-null    float64 
 13  vox_porcentaje             179 non-null    float64 
 14  pacma                      159 non-null    float64 
 15  pacma_porcentaje           159 non-null    float64 
 16  fe_de_las_jons             134 non-null    float64 
 17  fe_de_las_jons_porcentaje  134 non-null    float64 
 18  pum+j                      120 non-null    float64 
 19  pum+j_porcentaje           120 non-null    float64 
 20  pcte                       113 non-null    float64 
 21  pcte_porcentaje            113 non-null    float64 
 22  upyd                       120 non-null    float64 
 23  upyd_porcentaje            120 non-null    float64 
 24  ph                         96 non-null     float64 
 25  ph_porcentaje              96 non-null     float64 
 26  pcas_tc                    95 non-null     float64 
 27  pcas_tc_porcentaje         95 non-null     float64 
 28  p_lib                      90 non-null     float64 
 29  p_lib_porcentaje           90 non-null     float64 
 30  uleg                       71 non-null     float64 
 31  uleg_porcentaje            70 non-null     float64 
 32  escrutado                  179 non-null    float64 
 33  votos_totales              179 non-null    float64 
 34  votos_totales_porcentaje   179 non-null    float64 
 35  abstencion                 179 non-null    float64 
 36  abstencion_porcentaje      179 non-null    float64 
 37  votos_nulos                179 non-null    float64 
 38  votos_nulos_porcentaje     179 non-null    float64 
 39  votos_blancos              179 non-null    float64 
 40  votos_blancos_porcentaje   179 non-null    float64 
 41  geometry                   179 non-null    geometry
dtypes: float64(39), geometry(1), object(2)
memory usage: 60.1+ KB

1.1.-Resumen del procedimiento:

Todo el proceso hasta obtener una primera versión del dataframe se puede resumir en las siguientes funciones:

def table_escrutado(link):
    from bs4 import BeautifulSoup
    import requests
    
    html_text_municipio = requests.get(link).text
    soup = BeautifulSoup(html_text_municipio, 'lxml')

    table_escrutado = soup.find('table', {'id': 'tablaResumen'})
    table_escrutado_trs = table_escrutado.find_all('tr')
    
    results_table_escrutado = []
    for tr in table_escrutado_trs:
        local_table_escrutado = {}
        local_table_escrutado['encabezado'] = None if tr.select_one('.encabezado') is None else tr.select_one('.encabezado').text
        local_table_escrutado['numero'] = None if tr.select_one('.tipoNumero') is None else tr.select_one('.tipoNumero').text
        local_table_escrutado['porcentaje'] = None if tr.select_one('.tipoPorciento') is None else tr.select_one('.tipoPorciento').text
    
        results_table_escrutado.append(local_table_escrutado)
    
    return results_table_escrutado
def table_partido(link):
    from bs4 import BeautifulSoup
    import requests
    
    html_text_municipio = requests.get(link).text
    soup = BeautifulSoup(html_text_municipio, 'lxml')
    
    table_partido = soup.find('table', {'id': 'tablaVotosPartidos'})
    table_partido_trs = table_partido.find_all('tr')
    
    results_table_partido = []
    for tr in table_partido_trs:
        local_table_partido = {}
        local_table_partido['partido'] = None if tr.select_one('.nombrePartido') is None else tr.select_one('.nombrePartido').text
        local_table_partido['numero_votos'] = None if tr.select_one('.tipoNumeroVotos') is None else tr.select_one('.tipoNumeroVotos').text
        local_table_partido['porcentaje'] = None if tr.select_one('.tipoPorcientoVotos') is None else tr.select_one('.tipoPorcientoVotos').text
    
        results_table_partido.append(local_table_partido)
        
    return results_table_partido
def clean_strings_and_turn_float(value):
    if ' %' in value:
        return value.replace(' %', '').replace(',', '.')
    else:
        return value.replace('.', '')
    
def result_resume(results_table_escrutado):
    results_resume = []
    for result in results_table_escrutado:
        local_resume = {}
        if result.get('encabezado') == 'Escrutado:':
            local_resume['escrutado'] = clean_strings_and_turn_float(result.get('porcentaje'))
        if result.get('encabezado') == 'Votos contabilizados:':
            local_resume['votos_totales'] = clean_strings_and_turn_float(result.get('numero'))
            local_resume['votos_totales_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
        if result.get('encabezado') == 'Abstenciones:':
            local_resume['abstencion'] = clean_strings_and_turn_float(result.get('numero'))
            local_resume['abstencion_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
        if result.get('encabezado') == 'Votos nulos:':
            local_resume['votos_nulos'] = clean_strings_and_turn_float(result.get('numero'))
            local_resume['votos_nulos_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
        if result.get('encabezado') == 'Votos en blanco:':
            local_resume['votos_blancos'] = clean_strings_and_turn_float(result.get('numero'))
            local_resume['votos_blancos_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
        
        results_resume.append(local_resume)

    return results_resume
def strip_accents(text):
    import unicodedata
    try:
        text = unicode(text, 'utf-8')
    except NameError: # unicode is a default on python 3 
        pass

    text = unicodedata.normalize('NFD', text)\
           .encode('ascii', 'ignore')\
           .decode("utf-8")

    return str(text)
def result_partido_resume(results_table_partido):
    results_resume_partido = []
    for result in results_table_partido:
        local_resume = {}
        if result.get('partido') == None:
            continue
        else:
            partido = strip_accents(result.get('partido').lower().replace('-', '_').replace(' ', '_'))
            local_resume[partido] = clean_strings_and_turn_float(result.get('numero_votos'))
            local_resume[partido+'_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
    
        results_resume_partido.append(local_resume)

    return results_resume_partido
def prepare_data_from_web(url, lis):
    data_from_web = []
    for li in lis:
        for link in li.find_all('a'):
            local_result = {}
            local_result['municipio'] = link.text
            local_result['link'] = url+link.get('href')
            local_result['escrutinio'] = table_escrutado(local_result['link'])
            local_result['partidos'] = table_partido(local_result['link'])
            
            data_from_web.append(local_result)
        
    return data_from_web
def format_data(data_from_web):
    data_formatted = []
    for data in data_from_web:
        local_result = {}
        local_result['municipio'] = data['municipio']
        local_result['link'] = data['link']
        local_result['escrutinio'] = result_resume(data['escrutinio'])
        local_result['partidos'] = result_partido_resume(data['partidos'])
    
        data_formatted.append(local_result)
    
    return data_formatted
def data_frame_preparation(data_formatted):
    import pandas as pd
    ## dataframe preparation
    df_partidos = pd.json_normalize(data_formatted, record_path='partidos', meta=['municipio', 'link'])
    df_partidos = df_partidos.groupby('municipio').apply(lambda x: x.bfill().head(1)).reset_index(drop=True)
    
    df_escrutinio = pd.json_normalize(data_formatted, record_path='escrutinio', meta=['municipio'])
    df_escrutinio = df_escrutinio.groupby('municipio').apply(lambda x: x.bfill().head(1)).reset_index(drop=True)
    
    df = pd.merge(df_partidos, df_escrutinio, on='municipio', how='outer')
    ## columns position switched:
    first_column = df.pop('municipio')
    second_column = df.pop('link')
    df.insert(0, 'municipio', first_column)
    df.insert(1, 'link', second_column)
    
    return df
def extract_data_from_web(url):
    from bs4 import BeautifulSoup
    import requests
    # html code processing from url:
    html_text = requests.get(url).text
    soup = BeautifulSoup(html_text, 'lxml')
    ul = soup.select('ul.estirar')[1]
    lis = ul.find_all('li')
    # preparing data before formatting:
    data_from_web = prepare_data_from_web(url, lis)
    # data formatted:
    data_formatted = format_data(data_from_web)
    ## dataframe 
    df = data_frame_preparation(data_formatted)
    
    return df

1.2.-Preparación de los datos de 2021:

Aplicamos la función que resumen el procedimiento:

url = 'https://resultados.elpais.com/elecciones/2021/autonomicas/12/'
df_2021 = extract_data_from_web(url)

df_2021
municipio link pp pp_porcentaje vox vox_porcentaje mas_madrid mas_madrid_porcentaje psoe psoe_porcentaje ... pole_porcentaje escrutado votos_totales votos_totales_porcentaje abstencion abstencion_porcentaje votos_nulos votos_nulos_porcentaje votos_blancos votos_blancos_porcentaje
0 Ajalvir https://resultados.elpais.com/elecciones/2021/... 1148 49.4 340 14.63 330 14.2 325 13.98 ... NaN 100 2339 71.95 912 28.05 15 0.64 10 0.43
1 Alameda del Valle https://resultados.elpais.com/elecciones/2021/... 64 38.1 16 9.52 36 21.43 30 17.86 ... NaN 100 168 86.15 27 13.85 0 0 3 1.79
2 Alcalá de Henares https://resultados.elpais.com/elecciones/2021/... 42645 42.58 9735 9.72 15540 15.52 19926 19.9 ... 0.01 99.54 100932 74.12 35235 25.88 789 0.78 603 0.6
3 Alcobendas https://resultados.elpais.com/elecciones/2021/... 30588 49.6 5533 8.97 8212 13.32 10757 17.44 ... 0.01 100 62036 77.16 18367 22.84 363 0.59 297 0.48
4 Alcorcón https://resultados.elpais.com/elecciones/2021/... 39588 40.9 7841 8.1 16945 17.51 19798 20.45 ... 0.01 100 97563 76.87 29356 23.13 769 0.79 565 0.58
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
174 Villar del Olmo https://resultados.elpais.com/elecciones/2021/... 558 48.27 163 14.1 149 12.89 154 13.32 ... 0.09 100 1165 74.54 398 25.46 9 0.77 7 0.61
175 Villarejo de Salvanés https://resultados.elpais.com/elecciones/2021/... 1714 41.66 420 10.21 515 12.52 1031 25.06 ... 0.05 100 4150 76.01 1310 23.99 36 0.87 24 0.58
176 Villaviciosa de Odón https://resultados.elpais.com/elecciones/2021/... 9769 57.3 1900 11.14 1790 10.5 2104 12.34 ... NaN 100 17125 82.34 3674 17.66 75 0.44 55 0.32
177 Villavieja del Lozoya https://resultados.elpais.com/elecciones/2021/... 73 39.89 16 8.74 42 22.95 28 15.3 ... NaN 100 183 80.26 45 19.74 0 0 1 0.55
178 Zarzalejo https://resultados.elpais.com/elecciones/2021/... 315 36.21 61 7.01 179 20.57 148 17.01 ... NaN 100 879 72.11 340 27.89 9 1.02 2 0.23

179 rows × 51 columns

len(df_2021)
179

Se ve que el número de municipios coincide con los que tiene la CAM; aún así vamos a comprobar antes de añadir la información geoespacial.

1.2.1-Añadir información geoespacial al dataframe:

Vemos si pueden haber diferencias entre los nombres de los municipios:

no_intersection_map = set(map_municipios['municipio']).difference(set(df_2021['municipio']))
len(no_intersection_map)
no_intersection_map
{'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)',
 'Jurisdicción Mancomunada de Cerdedilla y Navacerrada'}

Son los mismos que antes, procedemos a mergear los dataframes:

import pandas as pd
df_2021 = pd.merge(df_2021, map_municipios, how='left', on='municipio')
df_2021.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 179 entries, 0 to 178
Data columns (total 52 columns):
 #   Column                                 Non-Null Count  Dtype   
---  ------                                 --------------  -----   
 0   municipio                              179 non-null    object  
 1   link                                   179 non-null    object  
 2   pp                                     179 non-null    object  
 3   pp_porcentaje                          179 non-null    object  
 4   vox                                    179 non-null    object  
 5   vox_porcentaje                         179 non-null    object  
 6   mas_madrid                             179 non-null    object  
 7   mas_madrid_porcentaje                  179 non-null    object  
 8   psoe                                   179 non-null    object  
 9   psoe_porcentaje                        179 non-null    object  
 10  podemos_iu                             178 non-null    object  
 11  podemos_iu_porcentaje                  178 non-null    object  
 12  cs                                     174 non-null    object  
 13  cs_porcentaje                          174 non-null    object  
 14  pacma                                  153 non-null    object  
 15  pacma_porcentaje                       153 non-null    object  
 16  fe_de_las_jons                         103 non-null    object  
 17  fe_de_las_jons_porcentaje              103 non-null    object  
 18  pcte                                   96 non-null     object  
 19  pcte_porcentaje                        96 non-null     object  
 20  ph                                     75 non-null     object  
 21  ph_porcentaje                          75 non-null     object  
 22  3e_en_accion                           100 non-null    object  
 23  3e_en_accion_porcentaje                100 non-null    object  
 24  eb                                     103 non-null    object  
 25  eb_porcentaje                          103 non-null    object  
 26  partido_autonomos                      110 non-null    object  
 27  partido_autonomos_porcentaje           110 non-null    object  
 28  p_lib                                  94 non-null     object  
 29  p_lib_porcentaje                       94 non-null     object  
 30  udec                                   93 non-null     object  
 31  udec_porcentaje                        93 non-null     object  
 32  pum+j                                  114 non-null    object  
 33  pum+j_porcentaje                       114 non-null    object  
 34  volt                                   109 non-null    object  
 35  volt_porcentaje                        109 non-null    object  
 36  recortes_cero_pcas_tc_gv_m             93 non-null     object  
 37  recortes_cero_pcas_tc_gv_m_porcentaje  93 non-null     object  
 38  pcoe_pcpe                              83 non-null     object  
 39  pcoe_pcpe_porcentaje                   83 non-null     object  
 40  pole                                   56 non-null     object  
 41  pole_porcentaje                        56 non-null     object  
 42  escrutado                              179 non-null    object  
 43  votos_totales                          179 non-null    object  
 44  votos_totales_porcentaje               179 non-null    object  
 45  abstencion                             179 non-null    object  
 46  abstencion_porcentaje                  179 non-null    object  
 47  votos_nulos                            179 non-null    object  
 48  votos_nulos_porcentaje                 179 non-null    object  
 49  votos_blancos                          179 non-null    object  
 50  votos_blancos_porcentaje               179 non-null    object  
 51  geometry                               179 non-null    geometry
dtypes: geometry(1), object(51)
memory usage: 74.1+ KB
# pasamos a valores numéricos todas las columnas excepto 'municipio', 'link' y 'geometry':
cols = df_2021.columns.drop(['municipio', 'link', 'geometry'])

df_2021[cols] = df_2021[cols].apply(pd.to_numeric, errors='coerce', axis=1)
df_2021.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 179 entries, 0 to 178
Data columns (total 52 columns):
 #   Column                                 Non-Null Count  Dtype   
---  ------                                 --------------  -----   
 0   municipio                              179 non-null    object  
 1   link                                   179 non-null    object  
 2   pp                                     179 non-null    float64 
 3   pp_porcentaje                          179 non-null    float64 
 4   vox                                    179 non-null    float64 
 5   vox_porcentaje                         179 non-null    float64 
 6   mas_madrid                             179 non-null    float64 
 7   mas_madrid_porcentaje                  179 non-null    float64 
 8   psoe                                   179 non-null    float64 
 9   psoe_porcentaje                        179 non-null    float64 
 10  podemos_iu                             178 non-null    float64 
 11  podemos_iu_porcentaje                  178 non-null    float64 
 12  cs                                     174 non-null    float64 
 13  cs_porcentaje                          174 non-null    float64 
 14  pacma                                  153 non-null    float64 
 15  pacma_porcentaje                       153 non-null    float64 
 16  fe_de_las_jons                         103 non-null    float64 
 17  fe_de_las_jons_porcentaje              103 non-null    float64 
 18  pcte                                   96 non-null     float64 
 19  pcte_porcentaje                        96 non-null     float64 
 20  ph                                     75 non-null     float64 
 21  ph_porcentaje                          75 non-null     float64 
 22  3e_en_accion                           100 non-null    float64 
 23  3e_en_accion_porcentaje                100 non-null    float64 
 24  eb                                     103 non-null    float64 
 25  eb_porcentaje                          103 non-null    float64 
 26  partido_autonomos                      110 non-null    float64 
 27  partido_autonomos_porcentaje           110 non-null    float64 
 28  p_lib                                  94 non-null     float64 
 29  p_lib_porcentaje                       94 non-null     float64 
 30  udec                                   93 non-null     float64 
 31  udec_porcentaje                        93 non-null     float64 
 32  pum+j                                  114 non-null    float64 
 33  pum+j_porcentaje                       114 non-null    float64 
 34  volt                                   109 non-null    float64 
 35  volt_porcentaje                        109 non-null    float64 
 36  recortes_cero_pcas_tc_gv_m             93 non-null     float64 
 37  recortes_cero_pcas_tc_gv_m_porcentaje  93 non-null     float64 
 38  pcoe_pcpe                              83 non-null     float64 
 39  pcoe_pcpe_porcentaje                   83 non-null     float64 
 40  pole                                   56 non-null     float64 
 41  pole_porcentaje                        56 non-null     float64 
 42  escrutado                              179 non-null    float64 
 43  votos_totales                          179 non-null    float64 
 44  votos_totales_porcentaje               179 non-null    float64 
 45  abstencion                             179 non-null    float64 
 46  abstencion_porcentaje                  179 non-null    float64 
 47  votos_nulos                            179 non-null    float64 
 48  votos_nulos_porcentaje                 179 non-null    float64 
 49  votos_blancos                          179 non-null    float64 
 50  votos_blancos_porcentaje               179 non-null    float64 
 51  geometry                               179 non-null    geometry
dtypes: float64(49), geometry(1), object(2)
memory usage: 74.1+ KB

2.-Contexto y análisis de los resultados

El pasado 4 de mayo de 2021 se celebraron elecciones autonómicas en la Comunidad de Madrid, donde el Partido Popular fue el partido más votado con Isabel Díaz Ayuso repitiendo como candidata.

Estas elecciones se celebraron en un contexto particular: la irrupción del COVID-19 en España y el fin del período de confinamiento estricto de 99 días.

Las últimas elecciones se celebraron en 2019 donde el PSOE encabezado por Ángel Gabilondo fue el partido más votado frente a un PP desgastado a nivel regional y nacional por los casos de corrupción que tenían como epicentro la Comunidad de Madrid, pero no pudo formar gobierno por el apoyo de Ciudadanos (Cs) a Isabel Díaz Ayuso a cambio de formar un gobierno autonómico de coalición. Similares gobiernos PP-Cs existían en otras regiones como Castilla y León o Murcia.

Precisamente en Murcia, debido a los rumores de negociaciones entre PSOE y Cs para retirar el apoyo al PP, llevaron a Isabel Díaz Ayuso a anticiparse y convocar elecciones para no perder el gobierno autonómico sabiendo que las encuestas le eran favorables por dos aspectos:

    1. la estrategia de oposición al Gobierno nacional por la gestión de la COVID-19 y canalizar el malestar que generó en muchos sectores el confinamiento.

    1. las encuestas que indicaban una total caída del apoyo electoral a Cs, su socio de gobierno.

Vamos a pasar a analizar los resultados y a comentarlos.

2.1.-Exposición de los resultados

En las elecciones autonómicas de 2019 los partidos que obtuvieron representación fueron PSOE, PP, Cs, Más Madrid, Vox y Podemos-IU. Por otro lado, en las elecciones de 2021 vemos el siguiente resultado: PP, Más Madrid, PSOE, Vox, Podemos-IU, desapareciendo Cs del mapa electoral.

Hay que indicar que para obtener representación institucional en las elecciones autonómicas hay que superar la barrera del 5% de votos. En las elecciones de 2019 se repartían 132 escaños, mientras que en 2021 por el aumento poblacional el reparto fue de 136 escaños.

Vamos a pasar a exponer los resultados absolutos por partidos tomando como referencia los resultados de cada unas de las elecciones.

import numpy as np
import matplotlib.pyplot as plt
from chart_studio import plotly
import plotly.graph_objs as go
from plotly import tools
from plotly.offline import iplot, init_notebook_mode
init_notebook_mode()
import plotly.graph_objects as go
parties=['pp', 'psoe', 'cs', 'mas_madrid', 'vox', 'podemos_iu']
final_results_2019 = [
    df_2019['pp'].sum(), 
    df_2019['psoe'].sum(),
    df_2019['cs'].sum(),
    df_2019['mas_madrid'].sum(),
    df_2019['vox'].sum(),
    df_2019['podemos_iu'].sum(),
]
final_results_2021 = [
    df_2021['pp'].sum(), 
    df_2021['psoe'].sum(),
    df_2021['cs'].sum(),
    df_2021['mas_madrid'].sum(),
    df_2021['vox'].sum(),
    df_2021['podemos_iu'].sum(),
]

fig = go.Figure(data=[
    go.Bar(name='2021', x=parties, y=final_results_2021),
    go.Bar(name='2019', x=parties, y=final_results_2019)
])
# Change the bar mode
fig.update_layout(title='Distribución del voto por partidos', barmode='group')
fig.show()

Para saber cuál es la variación de los porcentajes de cada partido vamos a calcularlos con respecto al total de votos de cada elección. Para ello extraemos los resultados totales de cada partido en cada elección y creamos una función que nos da la diferencia en votos y porcentajes totales.

# Dataset 2019

# excluimos las columnas de 'municipio', 'link' y 'geometry':
cols_2019 = df_2019.columns.drop(['municipio', 'link', 'geometry'])

# excluimos los porcentajes:
cols_votes_2019 = [col for col in cols_2019 if '_porcentaje' not in col]
cols_votes_2019 = [col for col in cols_votes_2019 if '_share' not in col]
# Dataset 2021

# excluimos las columnas de 'municipio', 'link' y 'geometry':
cols_2021 = df_2021.columns.drop(['municipio', 'link', 'geometry'])

# excluimos los porcentajes:
cols_votes_2021 = [col for col in cols_2021 if '_porcentaje' not in col]
cols_votes_2021 = [col for col in cols_votes_2019 if '_share' not in col]
def total_results(party, df_year):
    total_votes = df_year[party].sum()
    total_percentage = (df_year[party].sum()/(df_year['votos_totales'].sum()))*100
    
    total_votes = str("{:,}".format(total_votes)).strip('.0')+' votos'
    total_percentage = str(round(total_percentage, 2))+' %'
    
    return [total_votes, total_percentage]
def difference_elections(party, df_year_1, df_year_2):
    votes_difference = df_year_1[party].sum() - df_year_2[party].sum()
    
    percentage_1 = (df_year_1[party].sum()/(df_year_1['votos_totales'].sum()))*100
    percentage_2 = (df_year_2[party].sum()/(df_year_2['votos_totales'].sum()))*100
    percentage_difference = percentage_1 - percentage_2
    
    votes_difference = str("{:,}".format(votes_difference)).strip('.0')+' votos'
    percentage_difference = str(round(percentage_difference, 2))+' %'
    
    return [votes_difference, percentage_difference]

PSOE

print('PSOE_2019: '+ total_results('psoe', df_2019)[0] +' / '+ total_results('psoe', df_2019)[1])
print('PSOE_2021: '+ total_results('psoe', df_2021)[0] +' / '+ total_results('psoe', df_2021)[1])
print('PSOE_difference: '+ difference_elections('psoe', df_2021, df_2019)[0] +' / '+ difference_elections('psoe', df_2021, df_2019)[1])
PSOE_2019: 880,969 votos / 27.23 %
PSOE_2021: 610,19 votos / 16.74 %
PSOE_difference: -270,779 votos / -10.49 %

Ciudadanos

print('CS_2019: '+ total_results('cs', df_2019)[0] +' / '+ total_results('cs', df_2019)[1])
print('CS_2021: '+ total_results('cs', df_2021)[0] +' / '+ total_results('cs', df_2021)[1])
print('CS_difference: '+ difference_elections('cs', df_2021, df_2019)[0] +' / '+ difference_elections('cs', df_2021, df_2019)[1])
CS_2019: 626,766 votos / 19.37 %
CS_2021: 129,216 votos / 3.55 %
CS_difference: -497,55 votos / -15.83 %

Más Madrid

print('MAS_MADRID_2019: '+ total_results('mas_madrid', df_2019)[0] +' / '+ total_results('mas_madrid', df_2019)[1])
print('MAS_MADRID_2021: '+ total_results('mas_madrid', df_2021)[0] +' / '+ total_results('mas_madrid', df_2021)[1])
print('MAS_MADRID_difference: '+ difference_elections('mas_madrid', df_2021, df_2019)[0] +' / '+ difference_elections('mas_madrid', df_2021, df_2019)[1])
MAS_MADRID_2019: 472,221 votos / 14.6 %
MAS_MADRID_2021: 614,66 votos / 16.87 %
MAS_MADRID_difference: 142,439 votos / 2.27 %

PODEMOS-IU

print('PODEMOS_IU_2019: '+ total_results('podemos_iu', df_2019)[0] +' / '+ total_results('podemos_iu', df_2019)[1])
print('PODEMOS_IU_2021: '+ total_results('podemos_iu', df_2021)[0] +' / '+ total_results('podemos_iu', df_2021)[1])
print('PODEMOS_IU_difference: '+ difference_elections('podemos_iu', df_2021, df_2019)[0] +' / '+ difference_elections('podemos_iu', df_2021, df_2019)[1])
PODEMOS_IU_2019: 179,958 votos / 5.56 %
PODEMOS_IU_2021: 261,01 votos / 7.16 %
PODEMOS_IU_difference: 81,052 votos / 1.6 %

Vox

print('VOX_2019: '+ total_results('vox', df_2019)[0] +' / '+ total_results('vox', df_2019)[1])
print('VOX_2021: '+ total_results('vox', df_2021)[0] +' / '+ total_results('vox', df_2021)[1])
print('VOX_difference: '+ difference_elections('vox', df_2021, df_2019)[0] +' / '+ difference_elections('vox', df_2021, df_2019)[1])
VOX_2019: 285,836 votos / 8.84 %
VOX_2021: 330,66 votos / 9.07 %
VOX_difference: 44,824 votos / 0.24 %

Vemos claramente cómo el PP es el partido que más crece en número de votos (+910,607 votos / 22.33%), frente a Cs que es el que más disminuye su apoyo electoral (-501,023 votos / -15.84%) pasando de 631,117 votos (19.39%) a 130,094 votos (3.55%), lo que implica no llegar al 5% mínimo para obtener representación parlamentaria. Otro partido que pierde apoyos es el PSOE (-272,236 votos / -10.49%).

El resto de partidos aumenta sus apoyos: Más Madrid pasa de 474,725 votos (14.59%) a 618,285 votos (16.85%), lo que supone un incremento de 142,439 votos (2.27%), consiguiendo dar el sorpaso al PSOE dentro del bloque de la izquierda; le sigue Podemos-IU de 181,242 votos (5.57%) a 262,45 votos (7.15%), lo que supone un crecimiento en 81,208 votos (1.59%). Por último, Vox es el partido de los que obtiene representación que menos crece: 288,313 votos (8.86%) en 2019 a 333,447 votos (9.09%) en 2021, lo que supone un aumento de 44,824 votos (0.24%).

2.2.-Análisis de la distribución del voto

A continuación vamos a hacer un análisis de la distribución del voto y cómo ha ido variando entre estas dos elecciones.

Vamos a analizar el voto conjunto de PP y PSOE, partidos que desde la Transición se han ido repartiendo a nivel nacional, autonómico y provincial la mayoría de los votos en cada elección. Esto nos permitirá ver qué apoyo representan frente a los partidos que han surgido en la última década (Podemos, Cs, Vox y Más Madrid) o que planeaban desde la Transición un nuevo modelo constitucional frente al consenso de PP y PSOE, como es el caso de IU.

Por último analizaremos la distribución del voto en torno a los ejes derecha (PP, Cs, Vox) e izquierda (PSOE, Más Madrid, Podemos-IU). Vamos a escoger esta división porque es la que siempre se ha resaltado desde los medios de comunicación y confirmado muchas veces por la política de alianzas entre los diferentes partidos, aunque hay que indicar que Cs varias veces ha prestado apoyo al PSOE frente al PP y que hay muchos analistas que indican que las políticas económicas y sociales del PSOE no se deberían considerar dentro del eje de izquierdas.

PP-PSOE (vs) Más Madrid-Cs-Vox-Podemos-IU

# 2019:
pp_2019 = df_2019['pp'].sum()
psoe_2019 = df_2019['psoe'].sum()
cs_2019 = df_2019['cs'].sum()
mas_madrid_2019 = df_2019['mas_madrid'].sum()
podemos_iu_2019 = df_2019['podemos_iu'].sum()
vox_2019 = df_2019['podemos_iu'].sum()

pp_psoe_2019 = pp_2019 + psoe_2019
otros_partidos_2019 = cs_2019 + mas_madrid_2019 + podemos_iu_2019 + vox_2019
pp_psoe_percentage_2019 = (pp_psoe_2019/df_2019['votos_totales'].sum())*100
otros_partidos_percentage_2019 = (otros_partidos_2019/df_2019['votos_totales'].sum())*100

# 2021:
pp_2021 = df_2021['pp'].sum()
psoe_2021 = df_2021['psoe'].sum()
cs_2021 = df_2021['cs'].sum()
mas_madrid_2021 = df_2021['mas_madrid'].sum()
podemos_iu_2021 = df_2021['podemos_iu'].sum()
vox_2021 = df_2021['podemos_iu'].sum()

pp_psoe_2021 = pp_2021 + psoe_2021
otros_partidos_2021 = cs_2021 + mas_madrid_2021 + podemos_iu_2021 + vox_2021
pp_psoe_percentage_2021 = (pp_psoe_2021/df_2021['votos_totales'].sum())*100
otros_partidos_percentage_2021 = (otros_partidos_2021/df_2021['votos_totales'].sum())*100

# Diferencia entre 2019 y 2021
## diferencia de votos:
pp_psoe_vote_diff = pp_psoe_2021 - pp_psoe_2019
otros_partidos_vote_diff = otros_partidos_2021 - otros_partidos_2019
## diferencia de porcentajes:
pp_psoe_per_diff = pp_psoe_percentage_2021 - pp_psoe_percentage_2019
otros_per_diff = otros_partidos_percentage_2021 - otros_partidos_percentage_2019
import plotly.graph_objects as go
parties=['pp_psoe', 'otros_partidos']
final_results_2019 = [
    pp_psoe_2019, 
    otros_partidos_2019,
]
final_results_2021 = [
    pp_psoe_2021, 
    otros_partidos_2021,
]

fig = go.Figure(data=[
    go.Bar(name='2021', x=parties, y=final_results_2021),
    go.Bar(name='2019', x=parties, y=final_results_2019)
])
# Change the bar mode
fig.update_layout(title='PP-PSOE (vs) Más Madrid-Cs-Vox-Podemos-IU', barmode='group')
fig.show()
# 2019
pp_psoe_2019 = str("{:,}".format(pp_psoe_2019)).strip('.0')+' votos'
pp_psoe_percentage_2019 = str(round(pp_psoe_percentage_2019, 2))+' %'
otros_partidos_2019 = str("{:,}".format(otros_partidos_2019)).strip('.0')+' votos'
otros_partidos_percentage_2019 = str(round(otros_partidos_percentage_2019, 2))+' %'

# 2021
pp_psoe_2021 = str("{:,}".format(pp_psoe_2021)).strip('.0')+' votos'
pp_psoe_percentage_2021 = str(round(pp_psoe_percentage_2021, 2))+' %'
otros_partidos_2021 = str("{:,}".format(otros_partidos_2021)).strip('.0')+' votos'
otros_partidos_percentage_2021 = str(round(otros_partidos_percentage_2021, 2))+' %'

# Diferencia entre 2019 y 2021
## diferencia de votos:
pp_psoe_vote_diff = str("{:,}".format(pp_psoe_vote_diff)).strip('.0')+' votos'
otros_partidos_vote_diff = str("{:,}".format(otros_partidos_vote_diff)).strip('.0')+' votos'
## diferencia de porcentajes:
pp_psoe_per_diff = str(round(pp_psoe_per_diff, 2))+' %'
otros_per_diff = str(round(otros_per_diff, 2))+' %'
print('PP_PSOE_2019: '+ pp_psoe_2019 +' / '+ pp_psoe_percentage_2019)
print('PP_PSOE_2021: '+ pp_psoe_2021 +' / '+ pp_psoe_percentage_2021)
print('PP_PSOE_DIFF: '+ pp_psoe_vote_diff +' / '+ pp_psoe_per_diff)
PP_PSOE_2019: 1,598,013 votos / 49.4 %
PP_PSOE_2021: 2,230,403 votos / 61.2 %
PP_PSOE_DIFF: 632,39 votos / 11.8 %
print('OTROS_PARTIDOS_2019: '+ otros_partidos_2019 +' / '+ otros_partidos_percentage_2019)
print('OTROS_PARTIDOS_2021: '+ otros_partidos_2021 +' / '+ otros_partidos_percentage_2021)
print('OTROS_PARTIDOS_DIFF: '+ otros_partidos_vote_diff +' / '+ otros_per_diff)
OTROS_PARTIDOS_2019: 1,458,903 votos / 45.1 %
OTROS_PARTIDOS_2021: 1,265,896 votos / 34.73 %
OTROS_PARTIDOS_DIFF: -193,007 votos / -10.36 %

Podemos ver que de las elecciones de 2019 a las de 2021 se produce una reconfiguración de los partidos tradicionales del Régimen del 78 (PP-PSOE) al pasar de 1,606,519 votos (49.36%) en 2019 a 2,244,890 votos (61.2%) en 2021, lo que supone un aumento de 638,371 votos (11.84%) liderado por el espectacular aumento del PP.

Por otro lado, los partidos al margen de PP-PSOE pasan de acumular 1,468,326 votos (45.11%) en 2019 a 1,273,279 votos (34.71%) en 2021, lo que supone una disminución de 195,047 votos (-10.4%). El principal responsable de esta caída está en el desplome de Cs, ya que como vimos Más Madrid, Podemos-IU y Vox crecieron en apoyos.

Se podría decir que bastante del apoyo que obtuvo Cs en 2019 pasó al PP en 2021 y que parte del apoyo del PSOE pudo haberse repartido tanto hacia la izquierda (principalmente Más Madrid) como hacia la derecha (principalmente PP).

Derecha(PP-Cs-Vox) (vs) Izquierda(PSOE, Más Madrid, Podemos-IU)

# Partidos
## 2019
pp_2019 = df_2019['pp'].sum()
psoe_2019 = df_2019['psoe'].sum()
cs_2019 = df_2019['cs'].sum()
mas_madrid_2019 = df_2019['mas_madrid'].sum()
podemos_iu_2019 = df_2019['podemos_iu'].sum()
vox_2019 = df_2019['podemos_iu'].sum()
## 2021
pp_2021 = df_2021['pp'].sum()
psoe_2021 = df_2021['psoe'].sum()
cs_2021 = df_2021['cs'].sum()
mas_madrid_2021 = df_2021['mas_madrid'].sum()
podemos_iu_2021 = df_2021['podemos_iu'].sum()
vox_2021 = df_2021['podemos_iu'].sum()

# 2019:
## Derecha:
right_votes_2019 = pp_2019 + cs_2019 + vox_2019
right_percentage_2019 = (right_votes_2019/df_2019['votos_totales'].sum())*100
## Izquierda:
left_votes_2019 = psoe_2019 + mas_madrid_2019 + podemos_iu_2019
left_percentage_2019 = (left_votes_2019/df_2019['votos_totales'].sum())*100
## Derecha-Izquierda diferencia:
right_left_vote_diff_2019 = right_votes_2019 - left_votes_2019
right_left_perc_diff_2019 = right_percentage_2019 - left_percentage_2019

# 2021:
## Derecha:
right_votes_2021 = pp_2021 + cs_2021 + vox_2021
right_percentage_2021 = (right_votes_2021/df_2021['votos_totales'].sum())*100
## Izquierda:
left_votes_2021 = psoe_2021 + mas_madrid_2021 + podemos_iu_2021
left_percentage_2021 = (left_votes_2021/df_2021['votos_totales'].sum())*100
## Derecha-Izquierda diferencia:
right_left_vote_diff_2021 = right_votes_2021 - left_votes_2021
right_left_perc_diff_2021 = right_percentage_2021 - left_percentage_2021

# Diferencia entre 2019 y 2021:
## diferencia de votos
right_vote_diff = right_votes_2021 - right_votes_2019
left_vote_diff = left_votes_2021 - left_votes_2019
## diferencia de porcentajes
right_percentage_diff = right_percentage_2021 - right_percentage_2019
left_percentage_diff = left_percentage_2021 - left_percentage_2019
import plotly.graph_objects as go
parties=['derecha', 'izquierda']
final_results_2019 = [
    right_votes_2019, 
    left_votes_2019,
]
final_results_2021 = [
    right_votes_2021, 
    left_votes_2021,
]

fig = go.Figure(data=[
    go.Bar(name='2021', x=parties, y=final_results_2021),
    go.Bar(name='2019', x=parties, y=final_results_2019)
])
# Change the bar mode
fig.update_layout(title='Derecha (vs) Izquierda', barmode='group')
fig.show()
# 2019
## Derecha:
right_votes_2019 = str("{:,}".format(right_votes_2019)).strip('.0')+' votos'
right_percentage_2019 = str(round(right_percentage_2019, 2))+' %'
## Izquierda:
left_votes_2019 = str("{:,}".format(left_votes_2019)).strip('.0')+' votos'
left_percentage_2019 = str(round(left_percentage_2019, 2))+' %'
## Derecha-Izquierda diferencia
right_left_vote_diff_2019 = str("{:,}".format(right_left_vote_diff_2019)).strip('.0')+' votos'
right_left_perc_diff_2019 = str(round(right_left_perc_diff_2019, 2))+' %'

# 2021
## Derecha:
right_votes_2021 = str("{:,}".format(right_votes_2021)).strip('.0')+' votos'
right_percentage_2021 = str(round(right_percentage_2021, 2))+' %'
## Izquierda:
left_votes_2021 = str("{:,}".format(left_votes_2021)).strip('.0')+' votos'
left_percentage_2021 = str(round(left_percentage_2021, 2))+' %'
## Derecha-Izquierda diferencia
right_left_vote_diff_2021 = str("{:,}".format(right_left_vote_diff_2021)).strip('.0')+' votos'
right_left_perc_diff_2021 = str(round(right_left_perc_diff_2021, 2))+' %'

# Diferencia entre 2019-2021
## diferencia de votos
right_vote_diff = str("{:,}".format(right_vote_diff)).strip('.0')+' votos'
left_vote_diff = str("{:,}".format(left_vote_diff)).strip('.0')+' votos'
## diferencia de porcentajes
right_percentage_diff = str(round(right_percentage_diff, 2))+' %'
left_percentage_diff = str(round(left_percentage_diff, 2))+' %'
print('RIGHT_2019: '+ right_votes_2019 +' / '+ right_percentage_2019)
print('RIGHT_2021: '+ right_votes_2021 +' / '+ right_percentage_2021)
print('RIGHT_DIFF: '+ right_vote_diff +' / '+ right_percentage_diff)
RIGHT_2019: 1,523,768 votos / 47.1 %
RIGHT_2021: 2,010,439 votos / 55.16 %
RIGHT_DIFF: 486,671 votos / 8.06 %
print('LEFT_2019: '+ left_votes_2019 +' / '+ left_percentage_2019)
print('LEFT_2021: '+ left_votes_2021 +' / '+ left_percentage_2021)
print('LEFT_DIFF: '+ left_vote_diff +' / '+ left_percentage_diff)
LEFT_2019: 1,533,148 votos / 47.39 %
LEFT_2021: 1,485,86 votos / 40.77 %
LEFT_DIFF: -47,288 votos / -6.62 %
print('RIGHT_LEFT_DIFF_2019: '+ right_left_vote_diff_2019 +' / '+ right_left_perc_diff_2019)
print('RIGHT_LEFT_DIFF_2021: '+ right_left_vote_diff_2021 +' / '+ right_left_perc_diff_2021)
RIGHT_LEFT_DIFF_2019: -9,38 votos / -0.29 %
RIGHT_LEFT_DIFF_2021: 524,579 votos / 14.39 %

Podemos decir que el bloque de la derecha aumenta en 490,792 votos (8.07%) en 2021, mientras que el bloque de la izquierda cae en 47,468 votos (-6.63%). En 2019, la derecha representaba el 47.11% (1,533,343 votos) mientras que la izquierda un 47.36% (1,541,502 votos), prácticamente empatados con una diferencia del 0.25% (8,159 votos) a favor de la izquierda. En cambio en el 2021 la derecha representa un 55.18% (2,024,135 votos) frente al 40.73% (1,494,034 votos), una diferencia del 14.45% (530,101 votos) a favor de la derecha.

El aumento de la derecha está liderado por el crecimiento del PP, pero se ve atenuado por la fuerte caída de Cs y el poco crecimiento de Vox. Mientras tanto en la izquierda la caída se debe al desplome del PSOE principalmente, pero atenuado por los crecimientos de Más Madrid y Podemos-IU.

2.3.-Distribución espacial de los resultados

A continuación vamos a hacer representaciones espaciales sobre la distribución del voto por los municipios de Madrid. Se van a seguir comparativas parecidas a las del apartado anterior: PP (vs) PSOE, PP-PSOE (vs) Otros Partidos, Derecha (vs) Izquierda, comparación del PP con el resto de partidos.

Para la construcción de mapas interactivos que permitieran mostrar la distribución del voto en cada municipio se ha utilizado la librería BokehJS y los datos geoespaciales de nuestros dataframes (columna ‘geometry’).

# funcion que te devuelve nº de municipios donde gana un partido (party_1) sobre otro (party_2):
def won_municipalities(party_1, party_2, df):
    municipalities = []
    municipalities = df['municipio'][df[party_1] > df[party_2]].count()
    
    return municipalities

PP (vs) PSOE

# añado nuevas columnas
df_2019["pp_share"] = df_2019["pp"] / df_2019["votos_totales"]
df_2019["rel_pp_share"] = df_2019["pp"] / (df_2019["pp"]+df_2019["psoe"])
df_2019["psoe_share"] = df_2019["psoe"] / df_2019["votos_totales"]
df_2019["rel_psoe_share"] = df_2019["psoe"] / (df_2019["pp"]+df_2019["psoe"])
from geopandas import GeoDataFrame

# result
from bokeh.io import output_notebook
from bokeh.plotting import figure, ColumnDataSource
from bokeh.io import output_notebook, show, output_file
from bokeh.plotting import figure
from bokeh.models import GeoJSONDataSource, LinearColorMapper, ColorBar, HoverTool
from bokeh.palettes import brewer
output_notebook()
import json

result = GeoDataFrame(df_2019)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())

color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
                     border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('pp', '@pp'),
                               ('psoe','@psoe'),
                               ('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2019: PP (vs) PSOE", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
          fill_color = {'field' :'rel_psoe_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
Loading BokehJS ...
# añado nuevas columnas
df_2021["pp_share"] = df_2021["pp"] / df_2021["votos_totales"]
df_2021["rel_pp_share"] = df_2021["pp"] / (df_2021["pp"]+df_2021["psoe"])
df_2021["psoe_share"] = df_2021["psoe"] / df_2021["votos_totales"]
df_2021["rel_psoe_share"] = df_2021["psoe"] / (df_2021["pp"]+df_2021["psoe"])
result = GeoDataFrame(df_2021)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())

color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
                     border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('pp', '@pp'),
                               ('psoe','@psoe'),
                               ('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2021: PP (vs) PSOE", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
          fill_color = {'field' :'rel_psoe_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
# municipios ganados por el PP al PSOE en 2019
won_municipalities('pp', 'psoe', df_2019)
63
# municipios ganados por el PSOE al PP en 2019
won_municipalities('psoe', 'pp', df_2019)
114
# municipios ganados por el PP al PSOE en 2021
won_municipalities('pp', 'psoe', df_2021)
176
# municipios ganados por el PSOE al PP en 2021
won_municipalities('psoe', 'pp', df_2021)
2

Se puede apreciar la gran debacle del PSOE al pasar de 114 municipios en los que obtuvo más apoyos sobre el PP en 2019 a solo 2 en 2021, mientras que el PP pasa de 63 municipios en 2019 a 176 en 2021. En el municipio de Navarredonda y San Mamés se obtiene un empate técnico entre ambos partidos.

PP-PSOE (vs) Otros Partidos

# 2019
df_2019 = df_2019.fillna(0)
df_2019['pp_psoe'] = df_2019["pp"] + df_2019["psoe"]
df_2019['otros_partidos'] = df_2019["mas_madrid"] + df_2019["cs"] + df_2019["podemos_iu"] + df_2019["vox"]

df_2019["pp_psoe_share"] = df_2019["pp_psoe"] / df_2019["votos_totales"]
df_2019["rel_pp_psoe_share"] = df_2019["pp_psoe"] / (df_2019["pp"]+df_2019["otros_partidos"])
df_2019["otros_partidos_share"] = df_2019["otros_partidos"] / df_2019["votos_totales"]
df_2019["rel_otros_partidos_share"] = df_2019["otros_partidos"] / (df_2019["pp_psoe"]+df_2019["otros_partidos"])

# 2021
df_2021 = df_2021.fillna(0)
df_2021['pp_psoe'] = df_2021["pp"] + df_2021["psoe"]
df_2021['otros_partidos'] = df_2021["mas_madrid"] + df_2021["cs"] + df_2021["podemos_iu"] + df_2021["vox"]

df_2021["pp_psoe_share"] = df_2021["pp_psoe"] / df_2021["votos_totales"]
df_2021["rel_pp_psoe_share"] = df_2021["pp_psoe"] / (df_2021["pp"]+df_2021["otros_partidos"])
df_2021["otros_partidos_share"] = df_2021["otros_partidos"] / df_2021["votos_totales"]
df_2021["rel_otros_partidos_share"] = df_2021["otros_partidos"] / (df_2021["pp_psoe"]+df_2021["otros_partidos"])
result = GeoDataFrame(df_2019)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())

color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
                     border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('pp_psoe', '@pp_psoe'),
                               ('otros_partidos','@otros_partidos'),
                               ('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2019: PP-PSOE (vs) Otros Partidos", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
          fill_color = {'field' :'rel_otros_partidos_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
result = GeoDataFrame(df_2021)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())

color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
                     border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('pp_psoe', '@pp_psoe'),
                               ('otros_partidos','@otros_partidos'),
                               ('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2021: PP-PSOE (vs) Otros Partidos", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
          fill_color = {'field' :'rel_otros_partidos_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
# municipios ganados por PP-PSOE al resto de partidos en 2019
won_municipalities('pp_psoe', 'otros_partidos', df_2019)
121
# municipios ganados por el resto de partidos a PP-PSOE en 2019
won_municipalities('otros_partidos', 'pp_psoe', df_2019)
58
# municipios ganados por PP-PSOE al resto de partidos en 2021
won_municipalities('pp_psoe', 'otros_partidos', df_2021)
176
# municipios ganados por el resto de partidos a PP-PSOE en 2021
won_municipalities('otros_partidos', 'pp_psoe', df_2021)
3

Se ve la reconfiguración del eje PP-PSOE frente a otros partidos (Cs, Vox, Podemos-IU y Más Madrid), ya que se pasa de 121 municipios donde obtienen mayoría frente a 58 a 176 municipios frente a 3.

Derecha (vs) Izquierda

# 2019
df_2019['derecha'] = df_2019["pp"] + df_2019["cs"] + df_2019["vox"]
df_2019['izquierda'] = df_2019["psoe"]+ df_2019["mas_madrid"] + df_2019["podemos_iu"] 

df_2019["derecha_share"] = df_2019["derecha"] / df_2019["votos_totales"]
df_2019["rel_derecha_share"] = df_2019["derecha"] / (df_2019["derecha"]+df_2019["izquierda"])
df_2019["izquierda_share"] = df_2019["izquierda"] / df_2019["votos_totales"]
df_2019["rel_izquierda_share"] = df_2019["izquierda"] / (df_2019["derecha"]+df_2019["izquierda"])

# 2021
df_2021['derecha'] = df_2021["pp"] + df_2021["cs"] + df_2021["vox"]
df_2021['izquierda'] = df_2021["psoe"]+ df_2021["mas_madrid"] + df_2021["podemos_iu"] 

df_2021["derecha_share"] = df_2021["derecha"] / df_2021["votos_totales"]
df_2021["rel_derecha_share"] = df_2021["derecha"] / (df_2021["derecha"]+df_2021["izquierda"])
df_2021["izquierda_share"] = df_2021["izquierda"] / df_2021["votos_totales"]
df_2021["rel_izquierda_share"] = df_2021["izquierda"] / (df_2021["derecha"]+df_2021["izquierda"])
result = GeoDataFrame(df_2019)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())

color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
                     border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('derecha', '@derecha'),
                               ('izquierda','@izquierda'),
                               ('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2019: Derecha (vs) Izquierda", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
          fill_color = {'field' :'rel_izquierda_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
result = GeoDataFrame(df_2021)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())

color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
                     border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('derecha', '@derecha'),
                               ('izquierda','@izquierda'),
                               ('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2021: Derecha (vs) Izquierda", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
          fill_color = {'field' :'rel_izquierda_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
# municipios ganados por el eje de la derecha al eje de la izquierda en 2019
won_municipalities('derecha', 'izquierda', df_2019)
127
# municipios ganados por el eje de la izquierda al eje de la derecha en 2019
won_municipalities('izquierda', 'derecha', df_2019)
51
# municipios ganados por el eje de la derecha al eje de la izquierda en 2021
won_municipalities('derecha', 'izquierda', df_2021)
159
# municipios ganados por el eje de la izquierda al eje de la derecha en 2021
won_municipalities('izquierda', 'derecha', df_2021)
19

Se puede apreciar el gran avance general de la derecha en la Comunidad de Madrid: pasa de 127 municipios en 2019 a 159 en 2021; mientras que la izquierda pasa de 51 a solo 19 municipios.

PP (vs) Más Madrid

# 2019
df_2019["pp_share"] = df_2019["pp"] / df_2019["votos_totales"]
df_2019["rel_pp_share"] = df_2019["pp"] / (df_2019["pp"]+df_2019["mas_madrid"])
df_2019["mas_madrid_share"] = df_2019["mas_madrid"] / df_2019["votos_totales"]
df_2019["rel_mas_madrid_share"] = df_2019["mas_madrid"] / (df_2019["pp"]+df_2019["mas_madrid"])

# 2021
df_2021["pp_share"] = df_2021["pp"] / df_2021["votos_totales"]
df_2021["rel_pp_share"] = df_2021["pp"] / (df_2021["pp"]+df_2021["mas_madrid"])
df_2021["mas_madrid_share"] = df_2021["mas_madrid"] / df_2021["votos_totales"]
df_2021["rel_mas_madrid_share"] = df_2021["mas_madrid"] / (df_2021["pp"]+df_2021["mas_madrid"])
result = GeoDataFrame(df_2019)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())

color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
                     border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('pp', '@pp'),
                               ('mas_madrid','@mas_madrid'),
                               ('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2019: PP (vs) Más Madrid", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
          fill_color = {'field' :'rel_mas_madrid_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
result = GeoDataFrame(df_2021)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())

color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
                     border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('pp', '@pp'),
                               ('mas_madrid','@mas_madrid'),
                               ('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2021: PP (vs) Más Madrid", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
          fill_color = {'field' :'rel_mas_madrid_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
# municipios ganados por el PP a Más Madrid en 2019
won_municipalities('pp', 'mas_madrid', df_2019)
169
# municipios ganados por Más Madrid a el PP en 2019
won_municipalities('mas_madrid', 'pp', df_2019)
10
# municipios ganados por el PP a Más Madrid en 2019
won_municipalities('pp', 'mas_madrid', df_2021)
179
# municipios ganados por Más Madrid a el PP en 2019
won_municipalities('mas_madrid', 'pp', df_2021)
0

Más Madrid, que fue el partido dentro del eje de izquierdas y que consiguió dar el sorpaso al PSOE, pasó de tener 10 municipios donde sacó más votos que el PP en las elecciones de 2019 a ninguno en las elecciones de 2021. Por lo tanto, a pesar del sorpaso, el PP mantiene el tipo frente a todos los partidos de izquierdas.

Es importante destacar cómo Más Madrid ha conseguido tener su caladero de votos en los feudos tradicionales del PSOE (sureste del municipio de Madrid: Rivas, Coslada, San Fernando, Fuenlabrada, Getafe, Parla, Pinto y Mejorada del Campo).

Más Madrid (vs) Podemos-IU

Por último vamos a ver dentro del eje de izquierdas la distribución del voto a la izquierda del PSOE entre Podemos-IU y Más Madrid.

# 2019
df_2019["podemos_iu_share"] = df_2019["podemos_iu"] / df_2019["votos_totales"]
df_2019["rel_podemos_iu_share"] = df_2019["podemos_iu"] / (df_2019["podemos_iu"]+df_2019["mas_madrid"])
df_2019["mas_madrid_share"] = df_2019["mas_madrid"] / df_2019["votos_totales"]
df_2019["rel_mas_madrid_share"] = df_2019["mas_madrid"] / (df_2019["podemos_iu"]+df_2019["mas_madrid"])

# 2021
df_2021["podemos_iu_share"] = df_2021["podemos_iu"] / df_2021["votos_totales"]
df_2021["rel_podemos_iu_share"] = df_2021["podemos_iu"] / (df_2021["podemos_iu"]+df_2021["mas_madrid"])
df_2021["mas_madrid_share"] = df_2021["mas_madrid"] / df_2021["votos_totales"]
df_2021["rel_mas_madrid_share"] = df_2021["mas_madrid"] / (df_2021["podemos_iu"]+df_2021["mas_madrid"])
result = GeoDataFrame(df_2019)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())

color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
                     border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('podemos_iu', '@podemos_iu'),
                               ('mas_madrid','@mas_madrid'),
                               ('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2019: Más Madrid (vs) Podemos-IU", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
          fill_color = {'field' :'rel_mas_madrid_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
result = GeoDataFrame(df_2021)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())

color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
                     border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('podemos_iu', '@podemos_iu'),
                               ('mas_madrid','@mas_madrid'),
                               ('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2021: Más Madrid (vs) Podemos-IU", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
          fill_color = {'field' :'rel_mas_madrid_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
# municipios ganados por Más Madrid a Podemos-IU en 2019
won_municipalities('mas_madrid', 'podemos_iu', df_2019)
160
# municipios ganados por Podemos-IU a Más Madrid en 2019
won_municipalities('podemos_iu', 'mas_madrid', df_2019)
17
# municipios ganados por Más Madrid a Podemos-IU en 2021
won_municipalities('mas_madrid', 'podemos_iu', df_2021)
173
# municipios ganados por Podemos-IU a Más Madrid en 2021
won_municipalities('podemos_iu', 'mas_madrid', df_2021)
6

Vemos cómo el apoyo a Más Madrid crece entre las dos elecciones con respecto a Podemos-IU, ya que pasa de 160 municipios donde suma más votos frente a 17 a 173 contra 6. Esto puede reflejar que el ser parte del Gobierno a nivel nacional le ha pasado factura a Podemos-IU.

Destaca cómo Más Madrid es la opción favorita frente a Podemos-IU sobre todo en los principales núcleos urbanos, mientras que Podemos-IU consigue superar a Más Madrid en pequeños municipios alejados del municipio de Madrid, sobre todo en el norte.

2.4.-Análisis de la abstención

A continuación vamos a analizar la evolución de la abstención entre las 2 elecciones para ver hasta qué grado pudo influir entre un resultado u otro.

Definimos unas funciones para obtener la abstención electoral en votos y porcentajes entre las dos elecciones:

def electoral_abstention(df_year):
    abstention_total_votes = df_year['abstencion'].sum()
    abstention_total_percentage = (df_year['abstencion'].sum()/df_year['votos_totales'].sum())*100
    
    abstention_total_votes = str("{:,}".format(abstention_total_votes)).strip('.0')+' votos'
    abstention_total_percentage = str(round(abstention_total_percentage, 2))+' %'
    
    return [abstention_total_votes, abstention_total_percentage]
def dif_electoral_abstention(df_year_1, df_year_2):
    abstention_total_votes_1 = df_year_1['abstencion'].sum()
    abstention_total_percentage_1 = (df_year_1['abstencion'].sum()/df_year_1['votos_totales'].sum())*100
    
    abstention_total_votes_2 = df_year_2['abstencion'].sum()
    abstention_total_percentage_2 = (df_year_2['abstencion'].sum()/df_year_2['votos_totales'].sum())*100
    
    diff_abstention_votes = abstention_total_votes_1 - abstention_total_votes_2
    diff_abstention_percentage = abstention_total_percentage_1 - abstention_total_percentage_2
    
    abstencion_total_votes = str("{:,}".format(diff_abstention_votes)).strip('.0')+' votos'
    abstention_total_percentage = str(round(diff_abstention_percentage, 2))+' %'
    
    return [abstencion_total_votes, abstention_total_percentage]
print('Abstención 2019: '+ electoral_abstention(df_2019)[0] +' / '+ electoral_abstention(df_2019)[1])
print('Abstención 2021: '+ electoral_abstention(df_2021)[0] +' / '+ electoral_abstention(df_2021)[1])
print('Diferencia 2021-2019: '+ dif_electoral_abstention(df_2021, df_2019)[0] +' / '+ dif_electoral_abstention(df_2021, df_2019)[1])
Abstención 2019: 1,516,826 votos / 46.89 %
Abstención 2021: 1,135,201 votos / 31.15 %
Diferencia 2021-2019: -381,625 votos / -15.74 %

Podemos ver cómo la abstención pasa de representar el 46.89% del total en las elecciones del 2019 (1,516,826 votos) a el 31.15% (1,135,201 votos), lo que supone una caída porcentual del 15.74% (-381,625 votos). Usualmente se atribuyen una menor a abstención a una mayor movilización de la izquierda, pero aquí vemos lo contrario: en 2019 obtiene más votos el candidato del PSOE, mientras que en 2021 la candidata del PP.

Distribución espacial de la abstención

Vamos a representar cómo se distribuye la abstención sobre el mapa y analizaremos qué partido sale beneficiado de la reducción/aumento de la abstención en cada municipio:

# 2019
df_2019["abstencion_share"] = df_2019["abstencion"] / df_2019["votos_totales"]
df_2019["rel_abstencion_share"] = df_2019["abstencion"] / (df_2019["abstencion"]+df_2019["votos_totales"])
df_2019["votos_totales_share"] = df_2019["votos_totales"] / df_2019["votos_totales"]
df_2019["rel_votos_totales_share"] = df_2019["votos_totales"] / (df_2019["abstencion"]+df_2019["votos_totales"])

# 2021
df_2021["abstencion_share"] = df_2021["abstencion"] / df_2021["votos_totales"]
df_2021["rel_abstencion_share"] = df_2021["abstencion"] / (df_2021["abstencion"]+df_2021["votos_totales"])
df_2021["votos_totales_share"] = df_2021["votos_totales"] / df_2021["votos_totales"]
df_2021["rel_votos_totales_share"] = df_2021["votos_totales"] / (df_2021["abstencion"]+df_2021["votos_totales"])
result = GeoDataFrame(df_2019)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())

color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
                     border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('votos_totales', '@votos_totales'),
                               ('abstencion','@abstencion'),
                               ('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2019: distribución de la abstención", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
          fill_color = {'field' :'rel_votos_totales_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
result = GeoDataFrame(df_2021)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())

color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
                     border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('votos_totales', '@votos_totales'),
                               ('abstencion','@abstencion'),
                               ('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2021: distribución de la abstención", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
          fill_color = {'field' :'rel_votos_totales_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)

Podemos ver en 2019 cómo la abstención se centra sobre todo en grandes núcleos de población (municipio de Madrid y contiguos), así como en municipios del sur (p.ej. Colmenar de Oreja). Contrasta la alta participación de los pequeños municipios del norte de Madrid, Valdemaqueda, Valdaracete, Santorcaz y Olmeda de las Fuentes.

En cambio en 2021 podemos ver cómo la abstención general se reduce en los grandes núcleos de población, mientras se reduce en algunos municipios del norte.

import pandas as pd

df_abstenciones = pd.DataFrame()
df_abstenciones['municipio'] = df_2021['municipio']
df_abstenciones['votos_totales_2019'] = df_2019['votos_totales']
df_abstenciones['votos_totales_2021'] = df_2021['votos_totales']
df_abstenciones['abstencion_2019'] = df_2019['abstencion']
df_abstenciones['abstencion_2021'] = df_2021['abstencion']
df_abstenciones['dif_abstencion_votos'] = df_abstenciones['abstencion_2021'] - df_abstenciones['abstencion_2019']
df_abstenciones['dif_abstencion_porcentaje'] = df_2021['abstencion_porcentaje'] - df_2019['abstencion_porcentaje']
df_abstenciones.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 179 entries, 0 to 178
Data columns (total 7 columns):
 #   Column                     Non-Null Count  Dtype  
---  ------                     --------------  -----  
 0   municipio                  179 non-null    object 
 1   votos_totales_2019         179 non-null    float64
 2   votos_totales_2021         179 non-null    float64
 3   abstencion_2019            179 non-null    float64
 4   abstencion_2021            179 non-null    float64
 5   dif_abstencion_votos       179 non-null    float64
 6   dif_abstencion_porcentaje  179 non-null    float64
dtypes: float64(6), object(1)
memory usage: 11.2+ KB
# donde más crece la abstención en 2021 con respecto a 2019:
df_abstenciones.nlargest(15, ['dif_abstencion_porcentaje'])
municipio votos_totales_2019 votos_totales_2021 abstencion_2019 abstencion_2021 dif_abstencion_votos dif_abstencion_porcentaje
138 Somosierra 71.0 58.0 6.0 18.0 12.0 15.89
74 La Hiruela 55.0 51.0 5.0 14.0 9.0 13.21
117 Puebla de la Sierra 54.0 50.0 7.0 16.0 9.0 12.76
151 Valdaracete 434.0 385.0 51.0 108.0 57.0 11.39
99 Navarredonda y San Mamés 107.0 102.0 19.0 34.0 15.0 9.92
124 Robledillo de la Jara 51.0 58.0 9.0 19.0 10.0 9.68
47 El Atazar 65.0 64.0 14.0 24.0 10.0 9.55
72 La Acebeda 70.0 42.0 7.0 9.0 2.0 8.56
54 Estremera 708.0 677.0 204.0 302.0 98.0 8.48
116 Prádena del Rincón 89.0 100.0 8.0 20.0 12.0 8.42
111 Pinilla del Valle 145.0 140.0 15.0 29.0 14.0 7.78
18 Braojos 152.0 150.0 14.0 29.0 15.0 7.77
81 Lozoya 374.0 367.0 53.0 90.0 37.0 7.28
32 Cervera de Buitrago 108.0 104.0 21.0 31.0 10.0 6.68
126 Robregordo 33.0 35.0 14.0 19.0 5.0 5.40
# donde más se reduce la abstención en 2021 con respecto a 2019:
df_abstenciones.nsmallest(15, ['dif_abstencion_porcentaje'])
municipio votos_totales_2019 votos_totales_2021 abstencion_2019 abstencion_2021 dif_abstencion_votos dif_abstencion_porcentaje
167 Villalbilla 6579.0 8628.0 3186.0 1925.0 -1261.0 -14.39
57 Fuenlabrada 88021.0 105973.0 53332.0 35349.0 -17983.0 -12.72
12 Arroyomolinos 13661.0 16979.0 6875.0 4455.0 -2420.0 -12.70
45 Cubas de la Sagra 2808.0 3474.0 1569.0 1075.0 -494.0 -12.22
101 Nuevo Baztán 2729.0 3426.0 1692.0 1219.0 -473.0 -12.03
105 Parla 45596.0 55006.0 32908.0 23837.0 -9071.0 -11.69
65 Griñón 5044.0 6019.0 2492.0 1685.0 -807.0 -11.20
11 Arganda del Rey 22079.0 26481.0 12051.0 8455.0 -3596.0 -11.11
94 Móstoles 97110.0 113817.0 56442.0 39308.0 -17134.0 -11.09
16 Berzosa del Lozoya 97.0 116.0 84.0 64.0 -20.0 -10.85
44 Coslada 39726.0 45394.0 18155.0 11903.0 -6252.0 -10.60
71 Humanes de Madrid 8203.0 9619.0 4523.0 3233.0 -1290.0 -10.38
6 Algete 9817.0 11432.0 4698.0 3227.0 -1471.0 -10.36
133 San Sebastián de los Reyes 42567.0 49890.0 20915.0 14572.0 -6343.0 -10.34
25 Camarma de Esteruelas 3268.0 3900.0 1779.0 1296.0 -483.0 -10.31

Como podemos confirmar con la creación de este nuevo dataframe que resume la abstención entre las elecciones de 2019 y 2021: la abstención crece más en pequeños municipios y más en municipios que ya empiezan a tener una población considerable.

3.- Conclusiones

Después de haber analizado los resultados de las elecciones de 2019 y 2021 podemos concluir lo siguiente:

  • El PP es el indiscutible ganador de las elecciones de 2021 al ser el partido que más crece de todos los que consiguieron representación parlamentaria. La estrategia de Isabel Díaz Ayuso de anular a nivel interno a la oposición y convertirse a nivel nacional en el principal agente de oposición a la gestión de la pandemia por parte del Gobierno nacional, por encima incluso del líder a nivel nacional del PP, le ha reportado grandes beneficios.

  • El PSOE es el gran perdedor no solo por la pérdida de votos, sino también por la pérdida de apoyos tradicionales entre su electorado que ha optado por el voto castigo hacia el PP o la búsqueda de una nueva opción: Más Madrid. Muchos analistas coinciden en que la pérdida de apoyo viene de una falta de liderazgo por parte de Ángel Gabilondo frente a Isabel Díaz Ayuso, así como ser el partido a nivel nacional que ha gestionado la pandemia de COVID-19.

  • Ciudadanos sufre las consecuencias de formar parte de un gobierno de coalición donde no han sabido hacer frente al liderazgo de Isabel Díaz Ayuso, así como las consecuencias de una política errática a nivel nacional en la que no ha podido hacer frente al PP y al auge de Vox en sus campañas contra el Gobierno nacional en diferentes problemáticas nacionales (Cataluña, COVID-19, etc).

  • Más Madrid es el partido de izquierdas que cosecha más éxitos al aumentar su base electoral y conseguir dar el deseado sorpaso al PSOE que partidos como Podemos-IU han intentado y nunca han conseguido. Se ha beneficiado de ser un partido de izquierdas que no forma parte del Gobierno nacional de PSOE-UnidasPodemos. Por último, desde diferentes analistas se ha dicho que su candidata, Mónica García, era la candidata perfecta para el momento histórico que se vivía: una profesional sanitaria en el contexto de la pandemia de COVID-19.

  • Podemos-IU consigue aumentar mínimamente sus apoyos electorales por la participación de su líder fundador como candidato, Pablo Iglesias Turrión. Indudablemente ha conseguido movilizar a su base electoral para recuperar apoyos perdidos, ya que todas las encuestas indicaban que podían incluso desaparecer como Cs, pero aún así cabe preguntarse si su candidatura movilizó también el voto contra la izquierda a nivel general.

  • Vox a pesar de que en las pasadas elecciones de 2019 mostraron un fuerte músculo para poder crecer y situarse en las instituciones madrileñas, tampoco han podido escapar del liderazgo de Isabel Díaz Ayuso. Consiguen mantener su base de apoyo, pero no consiguen hacerla crecer ni un 1%.

  • En cuanto a la participación, las elecciones de 2021 han desmitificado que una participación alta sea sinónimo de un mayor apoyo hacia la izquierda vistos los resultados. Se podría decir que esta mayor movilización se ha debido a los liderazgos principales de Isabel Díaz Ayuso y Pablo Iglesias Turrión, que han movilizado el voto a uno y otro lado.

!pip list
Package                            Version
---------------------------------- -------------------
alabaster                          0.7.12
anaconda-client                    1.7.2
anaconda-navigator                 2.0.3
anaconda-project                   0.9.1
anyio                              2.2.0
appdirs                            1.4.4
argh                               0.26.2
argon2-cffi                        20.1.0
asn1crypto                         1.4.0
astroid                            2.5
astropy                            4.2.1
async-generator                    1.10
atomicwrites                       1.4.0
attrs                              20.3.0
autopep8                           1.5.6
Babel                              2.9.0
backcall                           0.2.0
backports.functools-lru-cache      1.6.4
backports.shutil-get-terminal-size 1.0.0
backports.tempfile                 1.0
backports.weakref                  1.0.post1
beautifulsoup4                     4.9.3
bitarray                           2.1.0
bkcharts                           0.2
black                              19.10b0
bleach                             3.3.0
bokeh                              2.3.2
boto                               2.49.0
Bottleneck                         1.3.2
brotlipy                           0.7.0
certifi                            2020.12.5
cffi                               1.14.5
chardet                            4.0.0
chart-studio                       1.1.0
click                              7.1.2
click-plugins                      1.1.1
cligj                              0.7.2
cloudpickle                        1.6.0
clyent                             1.2.2
colorama                           0.4.4
conda                              4.10.1
conda-build                        3.21.4
conda-content-trust                0+unknown
conda-package-handling             1.7.3
conda-repo-cli                     1.0.4
conda-token                        0.3.0
conda-verify                       3.4.2
contextlib2                        0.6.0.post1
cryptography                       3.4.7
cycler                             0.10.0
Cython                             0.29.23
cytoolz                            0.11.0
dask                               2021.4.0
decorator                          5.0.6
defusedxml                         0.7.1
diff-match-patch                   20200713
distributed                        2021.4.1
docutils                           0.16
entrypoints                        0.3
et-xmlfile                         1.0.1
fastcache                          1.1.0
filelock                           3.0.12
Fiona                              1.8.20
flake8                             3.9.0
Flask                              1.1.2
fsspec                             0.9.0
future                             0.18.2
geopandas                          0.9.0
gevent                             21.1.2
gitdb                              4.0.7
GitPython                          3.1.24
glob2                              0.7
gmpy2                              2.0.8
greenlet                           1.0.0
h5py                               2.10.0
HeapDict                           1.0.1
html5lib                           1.1
idna                               2.10
imageio                            2.9.0
imagesize                          1.2.0
importlib-metadata                 3.10.0
importlib-resources                5.2.2
iniconfig                          1.1.1
intervaltree                       3.1.0
ipykernel                          5.3.4
ipython                            7.22.0
ipython-genutils                   0.2.0
ipywidgets                         7.6.3
isort                              5.8.0
itsdangerous                       1.1.0
jdcal                              1.4.1
jedi                               0.17.2
jeepney                            0.6.0
Jinja2                             2.11.3
joblib                             1.0.1
json5                              0.9.5
jsonschema                         3.2.0
jupyter                            1.0.0
jupyter-book                       0.11.3
jupyter-cache                      0.4.3
jupyter-client                     6.1.12
jupyter-console                    6.4.0
jupyter-core                       4.7.1
jupyter-packaging                  0.7.12
jupyter-server                     1.4.1
jupyter-server-mathjax             0.2.3
jupyter-sphinx                     0.3.2
jupyterlab                         3.0.14
jupyterlab-pygments                0.1.2
jupyterlab-server                  2.4.0
jupyterlab-widgets                 1.0.0
jupytext                           1.10.3
keyring                            22.3.0
kiwisolver                         1.3.1
latexcodec                         2.0.1
lazy-object-proxy                  1.6.0
libarchive-c                       2.9
linkify-it-py                      1.0.1
llvmlite                           0.36.0
locket                             0.2.1
lxml                               4.6.3
markdown-it-py                     0.6.2
MarkupSafe                         1.1.1
matplotlib                         3.3.4
mccabe                             0.6.1
mdit-py-plugins                    0.2.6
mistune                            0.8.4
mkl-fft                            1.3.0
mkl-random                         1.2.1
mkl-service                        2.3.0
mock                               4.0.3
more-itertools                     8.7.0
mpmath                             1.2.1
msgpack                            1.0.2
multipledispatch                   0.6.0
munch                              2.5.0
mypy-extensions                    0.4.3
myst-nb                            0.12.3
myst-parser                        0.13.7
navigator-updater                  0.2.1
nbclassic                          0.2.6
nbclient                           0.5.3
nbconvert                          5.6.1
nbdime                             3.1.0
nbformat                           5.1.3
nest-asyncio                       1.5.1
networkx                           2.5
nltk                               3.6.1
nose                               1.3.7
notebook                           6.3.0
numba                              0.53.1
numexpr                            2.7.3
numpy                              1.20.1
numpydoc                           1.1.0
olefile                            0.46
openpyxl                           3.0.7
packaging                          20.9
pandas                             1.2.4
pandocfilters                      1.4.3
parso                              0.7.0
partd                              1.2.0
path                               15.1.2
pathlib2                           2.3.5
pathspec                           0.7.0
patsy                              0.5.1
pep8                               1.7.1
pexpect                            4.8.0
pickleshare                        0.7.5
Pillow                             8.2.0
pip                                21.0.1
pkginfo                            1.7.0
plotly                             5.0.0
pluggy                             0.13.1
ply                                3.11
prometheus-client                  0.10.1
prompt-toolkit                     3.0.17
psutil                             5.8.0
ptyprocess                         0.7.0
py                                 1.10.0
pybtex                             0.24.0
pybtex-docutils                    1.0.1
pycodestyle                        2.6.0
pycosat                            0.6.3
pycparser                          2.20
pycurl                             7.43.0.6
pydata-sphinx-theme                0.6.3
pydocstyle                         6.0.0
pyerfa                             1.7.3
pyflakes                           2.2.0
Pygments                           2.8.1
pylint                             2.7.4
pyls-black                         0.4.6
pyls-spyder                        0.3.2
pyodbc                             4.0.0-unsupported
pyOpenSSL                          20.0.1
pyparsing                          2.4.7
pyproj                             3.1.0
PyQt5                              5.12
PyQt5-Qt5                          5.15.2
PyQt5-sip                          4.19.19
PyQtWebEngine                      5.12
pyrsistent                         0.17.3
PySocks                            1.7.1
pytest                             6.2.3
python-dateutil                    2.8.1
python-jsonrpc-server              0.4.0
python-language-server             0.36.2
pytz                               2021.1
PyWavelets                         1.1.1
pyxdg                              0.27
PyYAML                             5.4.1
pyzmq                              20.0.0
QDarkStyle                         2.8.1
QtAwesome                          1.0.2
qtconsole                          5.0.3
QtPy                               1.9.0
regex                              2021.4.4
requests                           2.25.1
retrying                           1.3.3
rope                               0.18.0
Rtree                              0.9.7
ruamel-yaml-conda                  0.15.100
scikit-image                       0.18.1
scikit-learn                       0.24.1
scipy                              1.6.2
seaborn                            0.11.1
SecretStorage                      3.3.1
Send2Trash                         1.5.0
setuptools                         52.0.0.post20210125
Shapely                            1.7.1
simplegeneric                      0.8.1
singledispatch                     0.0.0
sip                                4.19.13
six                                1.15.0
smmap                              4.0.0
sniffio                            1.2.0
snowballstemmer                    2.1.0
sortedcollections                  2.1.0
sortedcontainers                   2.3.0
soupsieve                          2.2.1
Sphinx                             3.5.4
sphinx-book-theme                  0.1.4
sphinx-comments                    0.0.3
sphinx-copybutton                  0.4.0
sphinx-external-toc                0.2.3
sphinx-jupyterbook-latex           0.4.2
sphinx-multitoc-numbering          0.1.3
sphinx-panels                      0.5.2
sphinx-thebe                       0.0.10
sphinx-togglebutton                0.2.3
sphinxcontrib-applehelp            1.0.2
sphinxcontrib-bibtex               2.2.1
sphinxcontrib-devhelp              1.0.2
sphinxcontrib-htmlhelp             1.0.3
sphinxcontrib-jsmath               1.0.1
sphinxcontrib-qthelp               1.0.3
sphinxcontrib-serializinghtml      1.1.4
sphinxcontrib-websupport           1.2.4
spyder                             4.2.5
spyder-kernels                     1.10.2
SQLAlchemy                         1.4.15
statsmodels                        0.12.2
sympy                              1.8
tables                             3.6.1
tblib                              1.7.0
tenacity                           7.0.0
terminado                          0.9.4
testpath                           0.4.4
textdistance                       4.2.1
threadpoolctl                      2.1.0
three-merge                        0.1.1
tifffile                           2020.10.1
toml                               0.10.2
toolz                              0.11.1
tornado                            6.1
tqdm                               4.59.0
traitlets                          5.0.5
typed-ast                          1.4.2
typing-extensions                  3.7.4.3
uc-micro-py                        1.0.1
ujson                              4.0.2
unicodecsv                         0.14.1
urllib3                            1.26.4
watchdog                           1.0.2
wcwidth                            0.2.5
webencodings                       0.5.1
Werkzeug                           1.0.1
wheel                              0.36.2
widgetsnbextension                 3.5.1
wrapt                              1.12.1
wurlitzer                          2.1.0
xlrd                               2.0.1
XlsxWriter                         1.3.8
xlwt                               1.3.0
xmltodict                          0.12.0
yapf                               0.31.0
zict                               2.0.0
zipp                               3.4.1
zope.event                         4.5.0
zope.interface                     5.3.0